Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/scripts/discord-thread-validator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ export async function validateThreadChannel(threadId, prNumber, { botToken, foru
return false;
}
const VALIDATION_TIMEOUT_MS = 5_000;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
const res = await fetch(`https://discord.com/api/v10/channels/${threadId}`, {
headers: { Authorization: `Bot ${botToken}` },
signal: controller.signal,
});
clearTimeout(timeout);
if (!res.ok) {
warning(`Thread validation failed: channel ${threadId} returned ${res.status}`);
return false;
Expand All @@ -38,5 +37,12 @@ export async function validateThreadChannel(threadId, prNumber, { botToken, foru
} catch (err) {
warning(`Thread validation threw: ${err && err.message ? err.message : err}`);
return false;
} finally {
// `finally`, not a line after the await: when `fetch` rejects — a real network
// error — the timer would otherwise stay armed and fire `controller.abort()`
// five seconds later, long after this returned. Under Vitest that lands after
// the worker has torn down, which is an intermittent post-run error rather
// than a failing test. Same shape as `callDiscord` in discord-bot-api.mjs.
clearTimeout(timeout);
}
}
18 changes: 18 additions & 0 deletions .github/scripts/discord-thread-validator.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,22 @@ describe("validateThreadChannel", () => {
const result = await validateThreadChannel("500", number, { botToken });
expect(result).toBe(false);
});

it("leaves no timer armed once it has returned, on either path", async () => {
vi.useFakeTimers();
try {
vi.mocked(fetch).mockRejectedValue(new Error("network error"));
await validateThreadChannel("500", number, { botToken });
// The failing path is the one that used to leak: `clearTimeout` sat after
// the `await`, so a rejected fetch skipped it and left the 5s abort timer
// running past the end of the test.
expect(vi.getTimerCount()).toBe(0);

vi.mocked(fetch).mockResolvedValue({ ok: false, status: 404 });
await validateThreadChannel("404", number, { botToken });
expect(vi.getTimerCount()).toBe(0);
} finally {
vi.useRealTimers();
}
});
});
12 changes: 7 additions & 5 deletions .harness/docs/git-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,22 @@ Conventions for the Mavis reins when working in this repo.

## CI (`.github/workflows/ci.yml`)

CI runs on every PR to `main` and every push to `main`:
CI runs on every PR to `main`, `feat/ai-edition` and `release/**`, and on every push to those:
- `npm run lint` (Biome)
- `npx tsc --noEmit` (TypeScript)
- `npx tsc --noEmit` (TypeScript, app code)
- `npx tsc -p tsconfig.test.json --noEmit` (TypeScript, test files — a separate gate at zero)
- `npm run test` (Vitest unit)
- `npm run test:browser` (Vitest + Playwright headless)
- `npm run docs:check`
- `npx vite build` (renderer build smoke)
- `cargo test` / `cargo check` for the compositor on macOS, Windows and Linux

All five must be green before merge. Native helper code is NOT covered by CI — manual smoke test is required for `electron/*-helper/` changes; note it in the PR description.
All must be green before merge. Native helper code is NOT covered by CI — manual smoke test is required for `electron/*-helper/` changes; note it in the PR description.
Comment on lines +24 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the repository's native-helper paths.

Line 33 names electron/*-helper/, but the documented helpers live under electron/native/screencapturekit/ and electron/native/wgc-capture/. The current glob does not cover those paths. A native change can therefore bypass the manual smoke-test instruction.

Proposed wording
-Native helper code is NOT covered by CI — manual smoke test is required for `electron/*-helper/` changes; note it in the PR description.
+Native helper code is NOT covered by CI — manual smoke test is required for changes under `electron/native/`; note it in the PR description.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CI runs on every PR to `main`, `feat/ai-edition` and `release/**`, and on every push to those:
- `npm run lint` (Biome)
- `npx tsc --noEmit` (TypeScript)
- `npx tsc --noEmit` (TypeScript, app code)
- `npx tsc -p tsconfig.test.json --noEmit` (TypeScript, test files — a separate gate at zero)
- `npm run test` (Vitest unit)
- `npm run test:browser` (Vitest + Playwright headless)
- `npm run docs:check`
- `npx vite build` (renderer build smoke)
- `cargo test` / `cargo check` for the compositor on macOS, Windows and Linux
All five must be green before merge. Native helper code is NOT covered by CI — manual smoke test is required for `electron/*-helper/` changes; note it in the PR description.
All must be green before merge. Native helper code is NOT covered by CI — manual smoke test is required for `electron/*-helper/` changes; note it in the PR description.
CI runs on every PR to `main`, `feat/ai-edition` and `release/**`, and on every push to those:
- `npm run lint` (Biome)
- `npx tsc --noEmit` (TypeScript, app code)
- `npx tsc -p tsconfig.test.json --noEmit` (TypeScript, test files — a separate gate at zero)
- `npm run test` (Vitest unit)
- `npm run docs:check`
- `npx vite build` (renderer build smoke)
- `cargo test` / `cargo check` for the compositor on macOS, Windows and Linux
All must be green before merge. Native helper code is NOT covered by CI — manual smoke test is required for changes under `electron/native/`; note it in the PR description.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.harness/docs/git-workflow.md around lines 24 - 33, Update the native-helper
manual smoke-test instruction in the CI documentation to reference the
repository’s actual helper paths, electron/native/screencapturekit/ and
electron/native/wgc-capture/, instead of the incomplete electron/*-helper/ glob.
Preserve the requirement to note manual testing in the PR description.

Source: Coding guidelines


## Pull request flow

1. Branch from `main`.
2. Implement + add tests in the same package.
3. Run locally: `npm run lint && npx tsc --noEmit && npm run test`. For browser/e2e-touching changes, also run the relevant suite.
3. While implementing, run only the affected tests (`npx vitest --run <path>` or `--changed`); `npx tsc --noEmit` and `npm run lint` are the cheap inner-loop checks. Run the full `npm run test` **once**, here, before pushing.
4. Push and open the PR via `gh pr create`. Use `.github/pull_request_template.md`.
5. Wait for the Mavis reviewer (`openscreen-reviewer`) PASS or address the requested changes.
6. Merge once CI is green and review is PASS. PR titles must follow Conventional Commits (enforced by the `semantic-pr` job in `ci.yml`) — this keeps the auto-generated release notes clean.
Expand Down
4 changes: 3 additions & 1 deletion .harness/reins/openscreen-dev/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ You are the generalist implementer for the OpenScreen project — a free, open-s

- `npx tsc --noEmit` passes.
- `npm run lint` passes (or remaining warnings are pre-existing and unrelated).
- `npm run test` passes for any unit tests you added or affected.
- The tests you added or affected pass — run those files, `npx vitest --run <path>`, not the
whole suite. `npm run test` is minutes; run it once at the end if at all, and let CI be the
full-suite gate. Never `npm run test:watch` (it never terminates).
- The change is documented in the PR description (what + why + how to test).
- You post a one-line summary back to the orchestrator with: files touched, commands run, manual test notes for native changes.
17 changes: 12 additions & 5 deletions .harness/reins/openscreen-tester/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,31 @@ You are the test specialist for the OpenScreen project — a free, open-source s

## Scope

- **Own**: Vitest unit tests (`*.test.ts` / `*.test.tsx`, jsdom), Vitest browser tests (`vitest.browser.config.ts`, Playwright headless), Playwright e2e (`tests/e2e/`).
- **Own**: Vitest unit tests (`*.test.ts` / `*.test.tsx`), Playwright e2e (`tests/e2e/`).
- **Don't own**: writing production code (hand off to `openscreen-dev`). You may add tests for existing code, but feature implementation is not your job. Final PR quality gate is `openscreen-reviewer`.

## How you work

- Read `AGENTS.md` at the repo root for commands and conventions.
- Read `technical-documentation/testing/writing-tests.md` for the project's test style guide.
- Match the style of neighboring `*.test.<ext>` files in the same package — don't invent new patterns.
- Unit tests: `npm run test` (Vitest, jsdom). Browser tests: `npm run test:browser` (needs `npm run test:browser:install` once). E2E: `npm run test:e2e` (Playwright).
- Iterate with `npx vitest --run <path>` on the files you are writing. `npm run test` is the
whole suite (minutes) — run it once at the end, not between edits. E2E: `npm run test:e2e`.
- The Vitest environment is `node` by default. A test that needs a DOM opts in with
`// @vitest-environment jsdom` on line 1 — add it only when the test actually renders.
- E2E specs in `tests/e2e/windows-native-checklist.spec.ts` are Windows-only — gate with `test.skip` for other platforms rather than deleting.
- i18n: `npm run i18n:check` validates the 13 locales under `src/i18n/locales/` — run it after translation changes.
- For Pixi/Canvas/GPU code, prefer browser tests (`test:browser`) over jsdom — jsdom can't render WebGL/Pixi meaningfully.
- jsdom can't render WebGL/Pixi meaningfully and there is no browser-test tier anymore. Real
codec/GPU behavior belongs to the Rust suites in `crates/` or to the manual checklist —
don't write a jsdom test that pretends to cover it.
- Anything gated on `process.platform` must pin the platform in the test. CI is Linux-only,
so an unpinned Linux-only path is green in CI and red on every Windows and macOS machine.
- Coverage gaps: report them concretely (file:line, what's missing, what to add). Don't write the test for someone else's feature unprompted — flag it.

## Stop when

- `npm run test` passes.
- For browser-tested changes: `npm run test:browser` passes.
- The files you touched pass under `npx vitest --run <path>`, and one final `npm run test` is
green (that single full run is the gate — not one per edit).
- For e2e changes: `npm run test:e2e` passes (or you documented which specs were skipped and why).
- `npm run i18n:check` passes if any locale file was touched.
- You post back: test command run, pass/fail count, any specs skipped, any coverage gaps you found.
35 changes: 30 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ OpenScreen is a free, open-source screen recorder and video editor (Electron + R
- Start dev: `npm run dev` (Vite dev server; Electron window opens via `vite-plugin-electron`)
- Build: `npm run build` (TypeScript check + Vite build + electron-builder)
- Typecheck: `npx tsc --noEmit` — app code only. CI also runs `npx tsc -p tsconfig.test.json --noEmit` in a separate job ("Typecheck (tests)"), so **run both**: test files are invisible to the root config, and a type error in a `*.test.ts` fails CI while the root check stays green.
- Test (unit): `npm run test` (Vitest, jsdom env)
- Test (browser): `npm run test:browser` (Vitest + Playwright, requires `npm run test:browser:install` first)
- Test (unit): `npx vitest --run <path>` while you work, `npm run test` once at the end — see [Testing instructions](#testing-instructions)
- Test (e2e): `npm run test:e2e` (Playwright)
- Lint: `npm run lint` (Biome 2.4)
- Format: `npm run format` (Biome, tabs, double quotes, 100-col)
Expand Down Expand Up @@ -37,11 +36,37 @@ OpenScreen is a free, open-source screen recorder and video editor (Electron + R

## Testing instructions

- Unit tests live next to source as `*.test.ts` / `*.test.tsx` (Vitest, jsdom).
- Browser tests use `vitest.browser.config.ts` (Playwright headless) — only run when DOM/Pixi rendering matters.
### When to run what

The full unit suite is ~1670 tests over 140 files and takes over a minute. Running it after
every edit is the main way an agent turns a 5-minute task into a 30-minute one, so don't:

- **While you work** — run only what you touched: `npx vitest --run src/lib/foo.test.ts`,
or `npx vitest --run src/lib/ai-edition` for a directory. `npm run test:changed` picks
the affected files off the working tree, `npx vitest --run --changed main` off the
branch diff. A single file is 1–10s against ~80s for everything.
- **Typecheck and lint freely** — `npx tsc --noEmit` and `npm run lint` are seconds, not
minutes. They are the right inner-loop check, not the test suite.
- **Once, at the end** — `npm run test` before you commit or open the PR. One full run per
task, not per edit. If the change is narrow and CI will run anyway, the targeted run plus
CI is enough; say so rather than burning the wall-clock twice.
- **Never** `npm run test:watch` — it does not terminate, and it will hang the session.

### Layout and conventions

- Unit tests live next to source as `*.test.ts` / `*.test.tsx` (Vitest). Config is
`vitest.config.ts`; it covers `src/`, `electron/` and `.github/`.
- **The default environment is `node`.** A test that needs a DOM opts in with
`// @vitest-environment jsdom` on line 1 — that is also the fix for `document is not
defined`. Don't add it to a test that doesn't need it: jsdom setup dominates this
suite's runtime (see the comment in `vitest.config.ts`).
- Anything platform-conditional (`process.platform`) must pin the platform in the test.
CI is Linux-only, so a Linux-only code path left unpinned is green in CI and red on
every Windows and macOS machine — `electron/recording/webm-seek-index.test.ts` is the
worked example.
- E2E tests are in `tests/e2e/` (Playwright). Some specs are platform-specific (e.g. `windows-native-checklist.spec.ts`).
- Add a test for every new behavior in the same package as the code under test.
- All tests must pass before opening a PR. CI runs `npm run test` and `npm run test:browser` on every PR.
- All tests must pass before opening a PR. CI runs `npm run test` on every PR.

## Desktop E2E testing with computer-use

Expand Down
5 changes: 4 additions & 1 deletion electron/ai-edition/document-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,10 @@ describe("DocumentService", () => {
expect(onDisk.annotations).toHaveLength(120);
});

it("survives many interleaved saves of one project", async () => {
// 20 real temp-file+rename round trips, serialized through the save queue.
// That is genuinely more than 5s of disk when the rest of the suite is
// running in parallel, so it gets its own timeout rather than flaking.
it("survives many interleaved saves of one project", { timeout: 20_000 }, async () => {
const doc = await service.createProject("Storm");
// Sizes deliberately alternate long/short: equal-length writes overwrite
// each other cleanly and would prove nothing.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"build:whisper-binaries": "bash scripts/build-whisper-stt.sh",
"test:whisper-stt": "node scripts/test-whisper-stt.mjs",
"test": "vitest --run",
"test:changed": "vitest --run --changed",
"wb": "vitest --run --config vitest.workbench.config.ts",
"wb:l0": "vitest --run --config vitest.workbench.config.ts workbench/l0",
"wb:watch": "vitest --config vitest.workbench.config.ts workbench/l0",
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/CaptionsPane.gating.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
// Captions are a view of the transcript, so the pane's "Transcribe video"
// button is a retry, not a first step — the background pass has already tried.
// On a media with no audio track that retry can only fail again, so the button
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/ChatWelcome.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
// ChatWelcome guards the "no provider connected" empty state: the copy reaches
// the DOM, the CTA fires, and a non-English locale is really translated rather
// than falling back to English. localeParity.test.ts covers key presence for
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/ColorField.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/EditorEmptyState.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import type { ReactElement } from "react";
Expand Down
7 changes: 6 additions & 1 deletion src/components/ai-edition/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,12 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) {
action: {
label: t("exportDialog.showInFolder"),
onClick: () => {
void window.electronAPI?.revealInFolder?.(pickedPath);
// `revealInFolder` is a bare ipcRenderer.invoke, so it rejects when
// the main handler throws. The export already succeeded — failing to
// open the folder is not worth a second toast, but it is worth a line.
void window.electronAPI?.revealInFolder?.(pickedPath).catch((err) => {
console.warn("[export] failed to reveal the file in its folder:", err);
});
},
},
});
Expand Down
14 changes: 12 additions & 2 deletions src/components/ai-edition/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1512,7 +1512,13 @@ export function SourceTranscriptModal({
const v = videoRef.current;
if (!v) return;
if (v.paused) {
void v.play();
// Same catch as VirtualPreview's: `play()` rejects on the autoplay policy
// or when a new load interrupts it, and `isPlaying` is driven by the
// element's own play/pause events — so a rejection leaves nothing to
// reconcile, it just must not escape as an unhandled rejection.
void v.play().catch(() => {
// swallow: rejection just means playback never started
});
} else {
v.pause();
}
Expand All @@ -1528,7 +1534,11 @@ export function SourceTranscriptModal({
const requestFullscreen = () => {
const v = videoRef.current;
if (!v) return;
void v.requestFullscreen?.();
// Rejects when the gesture isn't accepted or the element can't go fullscreen.
// Nothing to reconcile — the document stays as it was.
void v.requestFullscreen?.().catch(() => {
// swallow: rejection just means we stayed windowed
});
};

return (
Expand Down
16 changes: 13 additions & 3 deletions src/components/ai-edition/NewEditorShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,13 @@ export function NewEditorShell() {
const togglePlay = useCallback(() => {
if (!videoElement) return;
if (videoElement.paused) {
void videoElement.play();
// Same catch as VirtualPreview's: `play()` rejects on the autoplay policy
// or when a new load interrupts it, and the store's `playing` flag is
// driven by the element's own play/pause listeners above — so a rejection
// leaves nothing to reconcile, it just must not escape unhandled.
void videoElement.play().catch(() => {
// swallow: rejection just means playback never started
});
} else {
videoElement.pause();
}
Expand Down Expand Up @@ -677,7 +683,9 @@ export function NewEditorShell() {
}
}
if (action === "record") {
void window.electronAPI?.startNewRecording?.();
void window.electronAPI?.startNewRecording?.().catch((err) => {
console.warn("[editor] failed to start a new recording:", err);
});
Comment on lines +686 to +688

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid starting the same recording twice after confirmation.

When the project is dirty, handleConfirmUnsaved calls startNewRecording, resolves the confirmation promise, and handleNewRecording calls startNewRecording again at Lines 699-701. This can start two recording flows.

Let handleNewRecording own the recording start after confirmation. Remove the action === "record" call from handleConfirmUnsaved.

Suggested fix
-			const { action, resolve } = unsavedPrompt;
+			const { resolve } = unsavedPrompt;
...
-				if (action === "record") {
-					void window.electronAPI?.startNewRecording?.().catch((err) => {
-						console.warn("[editor] failed to start a new recording:", err);
-					});
-				}
				resolve(choice);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ai-edition/NewEditorShell.tsx` around lines 686 - 688, Remove
the startNewRecording invocation from handleConfirmUnsaved when action ===
"record"; let handleNewRecording initiate recording after the unsaved-changes
confirmation resolves, while preserving the existing confirmation flow for other
actions.

}
resolve(choice);
})();
Expand All @@ -688,7 +696,9 @@ export function NewEditorShell() {
const handleNewRecording = useCallback(async () => {
const choice = await promptUnsaved("record");
if (choice !== "cancel") {
void window.electronAPI?.startNewRecording?.();
void window.electronAPI?.startNewRecording?.().catch((err) => {
console.warn("[editor] failed to start a new recording:", err);
});
}
}, [promptUnsaved]);

Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/NewProjectModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import type { ReactElement } from "react";
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/Preview.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/RightPanes.i18n.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
// Guards the right-rail settings panes against untranslated strings creeping
// back in: every pane used to hardcode its English labels (title, tabs, slider
// labels, help popover), so switching the app locale left the whole inspector
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/TranscriptPane.gating.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
// The transcript pane's empty state is what a user meets before any transcript
// exists — and, since transcription now runs by itself in the background, it is
// also where they wait for one. These assertions pin the three answers it has
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
// Backspace/Delete in the transcript pane, with the caret sitting BETWEEN words at editor
// level. That is where `restoreCaretBeforeWord` parks it after every cut, so it is the
// state the user is in when they hold Backspace to keep trimming — and the state where
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
// The rendered half of the shared-media cue bug. `findCueWordId` returning the right
// answer is not enough on its own: the cue id is compared against every rendered word,
// so while `data-word-id` carried the bare `word.id`, the SAME transcript word projected
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/TransportBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom";
import { act, cleanup, fireEvent, render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/WebcamOverlay.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom";
import { cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/backgroundImageUpload.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
// Cases recovered from the deleted `video-editor/backgroundImageUpload.test.ts`, whose
// module was dropped as dead code in the 2026-07-26 reorg — correctly, since only its own
// test imported it, but the empty-MIME fallback it encoded had never been wired into the
Expand Down
1 change: 1 addition & 0 deletions src/components/ai-edition/v4/EditorTopBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
Expand Down
Loading
Loading