Skip to content

Commit df89b21

Browse files
committed
E2E speedup (Step 6e): convert F-key tests to dispatchMenuCommand to eliminate keystroke-dispatch flake
Synthesized `tauriPage.keyboard.press('F5')` events don't always reach `handleGlobalKeyDown` under parallel-shard load — focus can drift after async MCP nav, and the document-level listener occasionally misses the event. The `dispatchMenuCommand` helper emits the `execute-command` Tauri event directly, mimicking the OS native menu accelerator path used in prod. It's unaffected by DOM focus state and parallel-load timing. Converted (28 dispatches across 16 tests where the test cares about the resulting dialog / file state, not the keyboard pathway): - conflict-copy.spec.ts: 7 × F5 → file.copy - conflict-move.spec.ts: 3 × F6 → file.move - conflict-edge-cases.spec.ts: 8 × F5 → file.copy - mtp-conflicts.spec.ts: 5 × F6 → file.move - accessibility.spec.ts: F5/F6/F8 → file.copy/move/delete, plus the ⌘F dispatch in openSearchDialog → search.open - file-watching.spec.ts: 1 × F5 → file.copy Kept on keyboard pathway (15 dispatches in tests that exist to verify the keyboard route itself): - app.spec.ts: 6 tests like "opens copy dialog with F5", "opens new folder dialog with F7", etc. - file-operations.spec.ts: 4 round-trip tests with "...via F5/F6/F2/F7" in their titles - mtp.spec.ts: 5 dispatches across the "renames file on MTP via keyboard" tests, the MTP delete dialog flow (comment marks it as full-keyboard), and "read-only storage rejects write operations" (verifies the read-only pre-check fires from the keyboard path) Validation — three back-to-back `./scripts/check.sh --check desktop-e2e-playwright` runs on native macOS with parallel shards: Pass 1: 131 passed, 0 failed, 0 flakes — 3m 13s Pass 2: 131 passed, 0 failed, 0 flakes — 3m 11s Pass 3: 131 passed, 0 failed, 0 flakes — 3m 10s Matches the 0/0/0 target. The keystroke-dispatch flake (0-1-2-5-10 flakes/run observed during Step 6a/6b/6d validation) is gone. No new flake categories surfaced. Full `./scripts/check.sh` sweep also green (45 checks in 2m 29s). Test files only — no app source changes.
1 parent 9c2e624 commit df89b21

7 files changed

Lines changed: 122 additions & 34 deletions

File tree

apps/desktop/test/e2e-playwright/accessibility.spec.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import path from 'path'
1616
import { fileURLToPath } from 'url'
1717
import { test, expect } from './fixtures.js'
1818
import {
19+
dispatchMenuCommand,
1920
ensureAppReady,
2021
navigateToRoute,
2122
executeViaCommandPalette,
@@ -147,9 +148,7 @@ async function openCommandPalette(tauriPage: PageLike): Promise<void> {
147148

148149
/** Open the search dialog overlay. */
149150
async function openSearchDialog(tauriPage: PageLike): Promise<void> {
150-
await tauriPage.evaluate(`document.dispatchEvent(new KeyboardEvent('keydown', {
151-
key: 'f', ctrlKey: ${String(CTRL_OR_META === 'Control')}, metaKey: ${String(CTRL_OR_META === 'Meta')}, bubbles: true
152-
}))`)
151+
await dispatchMenuCommand(tauriPage, 'search.open')
153152
await tauriPage.waitForSelector('.search-overlay', 5000)
154153
}
155154

@@ -209,7 +208,7 @@ for (const mode of ['light', 'dark'] as const) {
209208
await ensureAppReady(tauriPage)
210209
await moveCursorToFile(tauriPage, 'file-a.txt')
211210

212-
await tauriPage.keyboard.press('F5')
211+
await dispatchMenuCommand(tauriPage, 'file.copy')
213212
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
214213

215214
const { all } = await runAxeAudit(tauriPage, `Copy dialog (${mode})`, TRANSFER_DIALOG)
@@ -222,7 +221,7 @@ for (const mode of ['light', 'dark'] as const) {
222221
await ensureAppReady(tauriPage)
223222
await moveCursorToFile(tauriPage, 'file-a.txt')
224223

225-
await tauriPage.keyboard.press('F8')
224+
await dispatchMenuCommand(tauriPage, 'file.delete')
226225
const deleteDialog = '[data-dialog-id="delete-confirmation"]'
227226
await tauriPage.waitForSelector(deleteDialog, 5000)
228227

@@ -236,7 +235,7 @@ for (const mode of ['light', 'dark'] as const) {
236235
await ensureAppReady(tauriPage)
237236
await moveCursorToFile(tauriPage, 'file-a.txt')
238237

239-
await tauriPage.keyboard.press('F6')
238+
await dispatchMenuCommand(tauriPage, 'file.move')
240239
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
241240

242241
const { all } = await runAxeAudit(tauriPage, `Move dialog (${mode})`, TRANSFER_DIALOG)

apps/desktop/test/e2e-playwright/conflict-copy.spec.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77

88
import { test, expect } from './fixtures.js'
99
import { recreateFixtures } from '../e2e-shared/fixtures.js'
10-
import { ensureAppReady, getFixtureRoot, moveCursorToFile, pollUntil, sleep, TRANSFER_DIALOG } from './helpers.js'
10+
import {
11+
dispatchMenuCommand,
12+
ensureAppReady,
13+
getFixtureRoot,
14+
moveCursorToFile,
15+
pollUntil,
16+
sleep,
17+
TRANSFER_DIALOG,
18+
} from './helpers.js'
1119
import {
1220
createConflictFixturesA,
1321
createConflictFixturesB,
@@ -32,7 +40,7 @@ test.describe('Copy with conflict policies (Layout A)', () => {
3240
await ensureAppReady(tauriPage, { leftPane: ['readme.txt'] })
3341

3442
await selectAll(tauriPage)
35-
await tauriPage.keyboard.press('F5')
43+
await dispatchMenuCommand(tauriPage, 'file.copy')
3644

3745
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
3846
await waitForConflictPolicy(tauriPage)
@@ -60,7 +68,7 @@ test.describe('Copy with conflict policies (Layout A)', () => {
6068
await ensureAppReady(tauriPage, { leftPane: ['readme.txt'] })
6169

6270
await selectAll(tauriPage)
63-
await tauriPage.keyboard.press('F5')
71+
await dispatchMenuCommand(tauriPage, 'file.copy')
6472

6573
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
6674
await waitForConflictPolicy(tauriPage)
@@ -89,7 +97,7 @@ test.describe('Copy multi-item merge (Layout B)', () => {
8997
await ensureAppReady(tauriPage, { leftPane: ['alpha'] })
9098

9199
await selectAll(tauriPage)
92-
await tauriPage.keyboard.press('F5')
100+
await dispatchMenuCommand(tauriPage, 'file.copy')
93101

94102
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
95103
await waitForConflictPolicy(tauriPage)
@@ -119,7 +127,7 @@ test.describe('Copy multi-item merge (Layout B)', () => {
119127
await ensureAppReady(tauriPage, { leftPane: ['alpha'] })
120128

121129
await selectAll(tauriPage)
122-
await tauriPage.keyboard.press('F5')
130+
await dispatchMenuCommand(tauriPage, 'file.copy')
123131

124132
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
125133
await waitForConflictPolicy(tauriPage)
@@ -149,7 +157,7 @@ test.describe('Per-file conflict decisions (Layout A)', () => {
149157
await ensureAppReady(tauriPage, { leftPane: ['readme.txt'] })
150158

151159
await selectAll(tauriPage)
152-
await tauriPage.keyboard.press('F5')
160+
await dispatchMenuCommand(tauriPage, 'file.copy')
153161

154162
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
155163
await waitForConflictPolicy(tauriPage)
@@ -217,7 +225,7 @@ test.describe('Rename conflict resolution', () => {
217225
await ensureAppReady(tauriPage)
218226

219227
await moveCursorToFile(tauriPage, 'file-a.txt')
220-
await tauriPage.keyboard.press('F5')
228+
await dispatchMenuCommand(tauriPage, 'file.copy')
221229

222230
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
223231
await waitForConflictPolicy(tauriPage)
@@ -254,7 +262,7 @@ test.describe('Rename conflict resolution', () => {
254262
await ensureAppReady(tauriPage, { leftPane: ['readme.txt'] })
255263

256264
await selectAll(tauriPage)
257-
await tauriPage.keyboard.press('F5')
265+
await dispatchMenuCommand(tauriPage, 'file.copy')
258266

259267
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
260268
await waitForConflictPolicy(tauriPage)

apps/desktop/test/e2e-playwright/conflict-edge-cases.spec.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ import fs from 'fs'
99
import path from 'path'
1010
import { test, expect } from './fixtures.js'
1111
import { recreateFixtures } from '../e2e-shared/fixtures.js'
12-
import { ensureAppReady, getFixtureRoot, moveCursorToFile, pollUntil, sleep, TRANSFER_DIALOG } from './helpers.js'
12+
import {
13+
dispatchMenuCommand,
14+
ensureAppReady,
15+
getFixtureRoot,
16+
moveCursorToFile,
17+
pollUntil,
18+
sleep,
19+
TRANSFER_DIALOG,
20+
} from './helpers.js'
1321
import {
1422
createSymlinkFixture,
1523
createTypeMismatchFixture,
@@ -40,7 +48,7 @@ test.describe('Cancel and rollback', () => {
4048
const found = await moveCursorToFile(tauriPage, 'bulk')
4149
expect(found).toBe(true)
4250

43-
await tauriPage.keyboard.press('F5')
51+
await dispatchMenuCommand(tauriPage, 'file.copy')
4452
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
4553
await clickTransferStart(tauriPage)
4654

@@ -118,7 +126,7 @@ test.describe('Edge cases', () => {
118126

119127
// First copy: file-a.txt from left to right (no conflict)
120128
await moveCursorToFile(tauriPage, 'file-a.txt')
121-
await tauriPage.keyboard.press('F5')
129+
await dispatchMenuCommand(tauriPage, 'file.copy')
122130
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
123131
await clickTransferStart(tauriPage)
124132
await waitForDialogsToClose(tauriPage)
@@ -127,7 +135,7 @@ test.describe('Edge cases', () => {
127135

128136
// Second copy: same file again (now there IS a conflict)
129137
await moveCursorToFile(tauriPage, 'file-a.txt')
130-
await tauriPage.keyboard.press('F5')
138+
await dispatchMenuCommand(tauriPage, 'file.copy')
131139
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
132140
await waitForConflictPolicy(tauriPage)
133141
await selectConflictPolicy(tauriPage, 'overwrite')
@@ -147,7 +155,7 @@ test.describe('Edge cases', () => {
147155
await ensureAppReady(tauriPage)
148156

149157
await moveCursorToFile(tauriPage, 'file-a.txt')
150-
await tauriPage.keyboard.press('F5')
158+
await dispatchMenuCommand(tauriPage, 'file.copy')
151159

152160
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
153161
await waitForConflictPolicy(tauriPage)
@@ -169,7 +177,7 @@ test.describe('Symlink conflicts', () => {
169177
await ensureAppReady(tauriPage, { leftPane: ['link-target.txt'] })
170178

171179
await selectAll(tauriPage)
172-
await tauriPage.keyboard.press('F5')
180+
await dispatchMenuCommand(tauriPage, 'file.copy')
173181

174182
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
175183
await waitForConflictPolicy(tauriPage)
@@ -203,7 +211,7 @@ test.describe('Symlink conflicts', () => {
203211
await ensureAppReady(tauriPage, { leftPane: ['link-target.txt'] })
204212

205213
await selectAll(tauriPage)
206-
await tauriPage.keyboard.press('F5')
214+
await dispatchMenuCommand(tauriPage, 'file.copy')
207215

208216
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
209217
await waitForConflictPolicy(tauriPage)
@@ -229,7 +237,7 @@ test.describe('Type mismatch conflicts', () => {
229237
await ensureAppReady(tauriPage, { leftPane: ['reports.txt'] })
230238

231239
await selectAll(tauriPage)
232-
await tauriPage.keyboard.press('F5')
240+
await dispatchMenuCommand(tauriPage, 'file.copy')
233241

234242
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
235243
await waitForConflictPolicy(tauriPage)
@@ -294,7 +302,7 @@ test.describe('Type mismatch conflicts', () => {
294302
await tauriPage.waitForSelector('.file-pane .file-entry.is-under-cursor', 3000)
295303

296304
await selectAll(tauriPage)
297-
await tauriPage.keyboard.press('F5')
305+
await dispatchMenuCommand(tauriPage, 'file.copy')
298306

299307
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
300308

apps/desktop/test/e2e-playwright/conflict-move.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { test, expect } from './fixtures.js'
99
import { recreateFixtures } from '../e2e-shared/fixtures.js'
10-
import { ensureAppReady, getFixtureRoot, pollUntil, TRANSFER_DIALOG } from './helpers.js'
10+
import { dispatchMenuCommand, ensureAppReady, getFixtureRoot, pollUntil, TRANSFER_DIALOG } from './helpers.js'
1111
import {
1212
createConflictFixturesB,
1313
readFile,
@@ -30,7 +30,7 @@ test.describe('Move multi-item merge (Layout B)', () => {
3030
await ensureAppReady(tauriPage, { leftPane: ['alpha'] })
3131

3232
await selectAll(tauriPage)
33-
await tauriPage.keyboard.press('F6')
33+
await dispatchMenuCommand(tauriPage, 'file.move')
3434

3535
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
3636
await waitForConflictPolicy(tauriPage)
@@ -63,7 +63,7 @@ test.describe('Move multi-item merge (Layout B)', () => {
6363
await ensureAppReady(tauriPage, { leftPane: ['alpha'] })
6464

6565
await selectAll(tauriPage)
66-
await tauriPage.keyboard.press('F6')
66+
await dispatchMenuCommand(tauriPage, 'file.move')
6767

6868
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
6969
await waitForConflictPolicy(tauriPage)
@@ -99,7 +99,7 @@ test.describe('Move rollback', () => {
9999
await ensureAppReady(tauriPage, { leftPane: ['alpha'] })
100100

101101
await selectAll(tauriPage)
102-
await tauriPage.keyboard.press('F6')
102+
await dispatchMenuCommand(tauriPage, 'file.move')
103103

104104
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
105105
await waitForConflictPolicy(tauriPage)

apps/desktop/test/e2e-playwright/file-watching.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import path from 'path'
1414
import { test, expect } from './fixtures.js'
1515
import { recreateFixtures } from '../e2e-shared/fixtures.js'
1616
import {
17+
dispatchMenuCommand,
1718
ensureAppReady,
1819
getFixtureRoot,
1920
fileExistsInFocusedPane,
@@ -262,7 +263,7 @@ test.describe('File watching', () => {
262263
const found = await moveCursorToFile(tauriPage, 'file-a.txt')
263264
expect(found).toBe(true)
264265

265-
await tauriPage.keyboard.press('F5')
266+
await dispatchMenuCommand(tauriPage, 'file.copy')
266267
await tauriPage.waitForSelector(TRANSFER_DIALOG, 5000)
267268
await tauriPage.waitForSelector(`${TRANSFER_DIALOG} .btn-primary`, 3000)
268269
await tauriPage.click(`${TRANSFER_DIALOG} .btn-primary`)

apps/desktop/test/e2e-playwright/mtp-conflicts.spec.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ import {
2323
mcpAwaitItem,
2424
mcpSwitchPane,
2525
} from '../e2e-shared/mcp-client.js'
26-
import { ensureAppReady, getFixtureRoot, pollUntil, sleep, isStateClean, TRANSFER_DIALOG } from './helpers.js'
26+
import {
27+
dispatchMenuCommand,
28+
ensureAppReady,
29+
getFixtureRoot,
30+
pollUntil,
31+
sleep,
32+
isStateClean,
33+
TRANSFER_DIALOG,
34+
} from './helpers.js'
2735
import {
2836
waitForConflictPolicy,
2937
selectConflictPolicy,
@@ -118,7 +126,7 @@ test.describe('MTP cross-volume move conflicts', () => {
118126

119127
// Move report.txt to local right/ (which already has report.txt)
120128
await mcpCall('move_cursor', { pane: 'left', filename: 'report.txt' })
121-
await tauriPage.keyboard.press('F6')
129+
await dispatchMenuCommand(tauriPage, 'file.move')
122130

123131
await tauriPage.waitForSelector(TRANSFER_DIALOG, 10000)
124132
await waitForConflictPolicy(tauriPage)
@@ -160,7 +168,7 @@ test.describe('MTP cross-volume move conflicts', () => {
160168
await mcpAwaitItem('left', 'report.txt')
161169

162170
await mcpCall('move_cursor', { pane: 'left', filename: 'report.txt' })
163-
await tauriPage.keyboard.press('F6')
171+
await dispatchMenuCommand(tauriPage, 'file.move')
164172

165173
await tauriPage.waitForSelector(TRANSFER_DIALOG, 10000)
166174
await waitForConflictPolicy(tauriPage)
@@ -190,7 +198,7 @@ test.describe('MTP cross-volume move conflicts', () => {
190198
await mcpAwaitItem('right', 'file-a.txt', 15)
191199

192200
await mcpCall('move_cursor', { pane: 'left', filename: 'file-a.txt' })
193-
await tauriPage.keyboard.press('F6')
201+
await dispatchMenuCommand(tauriPage, 'file.move')
194202

195203
await tauriPage.waitForSelector(TRANSFER_DIALOG, 10000)
196204
await waitForConflictPolicy(tauriPage)
@@ -250,7 +258,7 @@ test.describe('MTP same-volume move conflicts', () => {
250258
await sleep(200)
251259

252260
await mcpCall('move_cursor', { pane: 'left', filename: 'report.txt' })
253-
await tauriPage.keyboard.press('F6')
261+
await dispatchMenuCommand(tauriPage, 'file.move')
254262

255263
await tauriPage.waitForSelector(TRANSFER_DIALOG, 10000)
256264
await waitForConflictPolicy(tauriPage)
@@ -302,7 +310,7 @@ test.describe('MTP same-volume move conflicts', () => {
302310
await sleep(200)
303311

304312
await mcpCall('move_cursor', { pane: 'left', filename: 'report.txt' })
305-
await tauriPage.keyboard.press('F6')
313+
await dispatchMenuCommand(tauriPage, 'file.move')
306314

307315
await tauriPage.waitForSelector(TRANSFER_DIALOG, 10000)
308316
await waitForConflictPolicy(tauriPage)

docs/notes/speed-up-e2e-tests.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,3 +875,67 @@ Two back-to-back full-suite runs (`./scripts/check.sh --check desktop-e2e-playwr
875875
The fix shifts the Cancel-copy test from a flaky 32.7 s outlier to either ~750 ms (rollback ran) or ~31 s (escape
876876
hatch). The escape-hatch duration is bounded by the test's own 30 s modal-close wait, not by anything we control here.
877877
Tightening that timeout would be a Step 7 candidate — the rollback path itself completes in milliseconds.
878+
879+
## After Step 6e (F-key tests via dispatchMenuCommand)
880+
881+
### Goal
882+
883+
Eliminate the residual "synthesized KeyboardEvent doesn't always reach its handler under parallel-shard load" flake
884+
observed in Steps 6a/6b/6d (0-1-2-5-10 flakes across runs, all clustered on `wait_for_selector` timeouts after an F-key
885+
dispatch).
886+
887+
### Approach
888+
889+
Replace `tauriPage.keyboard.press('F5')`-style synthesized keystrokes with `dispatchMenuCommand(tauriPage, 'file.copy')`
890+
— the helper in `helpers.ts` that emits the `execute-command` Tauri event directly, mimicking what the OS native menu
891+
accelerator does in prod. The Tauri-event path is unaffected by DOM focus state and parallel-load timing.
892+
893+
Rule of thumb: convert when the test cares about the resulting dialog / file state, keep keyboard when the test's title
894+
or comments mark it as exercising the keyboard pathway itself (e.g. `app.spec.ts` "opens copy dialog with F5",
895+
`file-operations.spec.ts` "...via F5", MTP read-only enforcement tests, MTP "renames file...via keyboard").
896+
897+
### What changed
898+
899+
**Converted to `dispatchMenuCommand` (28 dispatches across 16 tests):**
900+
901+
- `conflict-copy.spec.ts`: 7 × F5 → `file.copy`
902+
- `conflict-move.spec.ts`: 3 × F6 → `file.move`
903+
- `conflict-edge-cases.spec.ts`: 8 × F5 → `file.copy`
904+
- `mtp-conflicts.spec.ts`: 5 × F6 → `file.move`
905+
- `accessibility.spec.ts`: F5 → `file.copy`, F6 → `file.move`, F8 → `file.delete`, plus the ⌘F dispatch in
906+
`openSearchDialog()``search.open`
907+
- `file-watching.spec.ts`: 1 × F5 → `file.copy` (the "in-app copy without duplicates" test)
908+
909+
**Kept on keyboard pathway (15 dispatches across 15 tests):**
910+
911+
- `app.spec.ts` (6): "opens new folder dialog with F7" ×2, "opens copy dialog with F5", "opens move dialog with F6",
912+
"Cancel button closes the new folder dialog" (uses F7), "opens the delete confirmation dialog with F8"
913+
- `file-operations.spec.ts` (4): "...via F5", "...via F6", "...via F2", "...via F7" — round-trip tests with explicit
914+
F-key intent in their titles
915+
- `mtp.spec.ts` (5): F8 in "deletes file on MTP with 'Delete permanently' dialog" (comment marks it as full-keyboard
916+
flow), F2 ×2 in "renames file on MTP via keyboard" and "rename to existing name is rejected on MTP", F7 and F2 in
917+
"read-only storage rejects write operations" (test verifies the read-only pre-check fires from the keyboard path)
918+
919+
### Validation
920+
921+
Three back-to-back `./scripts/check.sh --check desktop-e2e-playwright` runs on native macOS with parallel shards:
922+
923+
| Run | Result | Total | Per shard | Flakes |
924+
| ------ | ------ | ----- | -------------------- | ------ |
925+
| Pass 1 || 3m13s | 131 passed, 0 failed | 0 |
926+
| Pass 2 || 3m11s | 131 passed, 0 failed | 0 |
927+
| Pass 3 || 3m10s | 131 passed, 0 failed | 0 |
928+
929+
**The keystroke-dispatch flake is gone.** All three runs match the 0/0/0 target. No new flake categories surfaced.
930+
`./scripts/check.sh` (full sweep) is green.
931+
932+
### Files touched
933+
934+
- `apps/desktop/test/e2e-playwright/conflict-copy.spec.ts`
935+
- `apps/desktop/test/e2e-playwright/conflict-move.spec.ts`
936+
- `apps/desktop/test/e2e-playwright/conflict-edge-cases.spec.ts`
937+
- `apps/desktop/test/e2e-playwright/mtp-conflicts.spec.ts`
938+
- `apps/desktop/test/e2e-playwright/accessibility.spec.ts`
939+
- `apps/desktop/test/e2e-playwright/file-watching.spec.ts`
940+
941+
No app source changes — test files only, per Step 6e constraints.

0 commit comments

Comments
 (0)