Skip to content

Commit 3bbf6e2

Browse files
test(ci): convert logic-heavy Playwright specs to Vitest and optimize CI pipeline (TASK-322, TASK-323, TASK-324)
Move pure-logic Playwright tests to Vitest unit tests, reduce visual/CSS specs, and restructure CI for faster PR checks. TASK-322: Convert sorting-ignore-words and keyboard-shortcuts logic to Vitest (new sorting-ignore-words.test.js, extended shortcuts.test.js). Reduce playback.spec.js from 1310 to 361 lines (hardware-dependent only), visual-regression.spec.js from 540 to 135 lines (8 critical snapshots), startup-fouc.spec.js from 441 to 151 lines (4 critical tests). Net: ~2,639 Playwright lines removed, 42 new Vitest tests added. TASK-323: Replace cargo tarpaulin with cargo nextest for PR checks (saves ~1.5 min). Move tarpaulin coverage to main-push-only job. Add dorny/paths-filter to gate Playwright on frontend changes. TASK-324: Tracking task complete — all three phases done. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 99abfb7 commit 3bbf6e2

11 files changed

Lines changed: 595 additions & 2851 deletions

.github/workflows/test.yml

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,27 @@ jobs:
5757
- name: Cargo clippy
5858
run: cargo clippy --workspace --all-features -- -D warnings
5959

60-
# cargo-binstall and cargo-tarpaulin are installed separately from the
61-
# composite action (which runs in check mode and skips cargo tools).
62-
# These are needed only for coverage — not for fmt, clippy, or check.
60+
- name: Run Rust tests
61+
run: cargo nextest run --workspace
62+
63+
# Coverage runs only on main push (not PRs) to keep the critical path fast
64+
rust-coverage:
65+
name: Rust Coverage
66+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
67+
runs-on: [macOS, ARM64]
68+
timeout-minutes: 25
69+
env:
70+
CARGO_INCREMENTAL: 1
71+
CARGO_TERM_COLOR: always
72+
73+
steps:
74+
- uses: actions/checkout@v6
75+
76+
- name: Setup Tauri build environment
77+
uses: ./.github/actions/setup-tauri-build
78+
with:
79+
mode: check
80+
6381
- uses: cargo-bins/cargo-binstall@main
6482

6583
- name: Install cargo-tarpaulin
@@ -213,8 +231,26 @@ jobs:
213231
path: app/frontend/coverage/
214232
retention-days: 30
215233

234+
# Detect frontend changes for conditional Playwright execution
235+
changes:
236+
name: Detect Changes
237+
runs-on: blacksmith-4vcpu-ubuntu-2404
238+
timeout-minutes: 2
239+
outputs:
240+
frontend: ${{ steps.filter.outputs.frontend }}
241+
steps:
242+
- uses: actions/checkout@v6
243+
- uses: dorny/paths-filter@v3
244+
id: filter
245+
with:
246+
filters: |
247+
frontend:
248+
- 'app/frontend/**'
249+
216250
playwright-tests:
217251
name: Playwright E2E Tests
252+
needs: [changes]
253+
if: needs.changes.outputs.frontend == 'true' || github.event_name == 'push'
218254
runs-on: [macOS, ARM64]
219255
timeout-minutes: 20
220256

app/frontend/__tests__/shortcuts.test.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ describe('keyboard shortcuts', () => {
9090
mockUi.modal = null;
9191
mockQueue.stopAfterCurrent = false;
9292
mockLibrary.searchQuery = '';
93+
mockPlayer.volume = 50;
94+
mockPlayer.currentTime = 30000;
9395
initKeyboardShortcuts();
9496
});
9597

@@ -180,6 +182,115 @@ describe('keyboard shortcuts', () => {
180182
});
181183
});
182184

185+
describe('Escape context-aware behavior', () => {
186+
it('should close settings when view is settings', () => {
187+
mockUi.view = 'settings';
188+
const event = createKeyEvent('Escape', { key: 'Escape' });
189+
keydownHandler(event);
190+
expect(mockUi.toggleSettings).toHaveBeenCalled();
191+
expect(event.preventDefault).toHaveBeenCalled();
192+
});
193+
194+
it('should close modal when modal is open', () => {
195+
mockUi.modal = 'someModal';
196+
const event = createKeyEvent('Escape', { key: 'Escape' });
197+
keydownHandler(event);
198+
expect(mockUi.closeModal).toHaveBeenCalled();
199+
expect(event.preventDefault).toHaveBeenCalled();
200+
});
201+
202+
it('should clear search query when no modal or settings open', () => {
203+
mockLibrary.searchQuery = 'test';
204+
const event = createKeyEvent('Escape', { key: 'Escape' });
205+
keydownHandler(event);
206+
expect(mockLibrary.searchQuery).toBe('');
207+
expect(mockLibrary.search).toHaveBeenCalledWith('');
208+
});
209+
210+
it('should do nothing when no modal, settings, or search active', () => {
211+
mockLibrary.searchQuery = '';
212+
const event = createKeyEvent('Escape', { key: 'Escape' });
213+
keydownHandler(event);
214+
expect(mockUi.toggleSettings).not.toHaveBeenCalled();
215+
expect(mockUi.closeModal).not.toHaveBeenCalled();
216+
});
217+
});
218+
219+
describe('volume and seek shortcuts', () => {
220+
it('should increase volume on ArrowUp', () => {
221+
const event = createKeyEvent('ArrowUp', { key: 'ArrowUp' });
222+
keydownHandler(event);
223+
expect(mockPlayer.setVolume).toHaveBeenCalledWith(55);
224+
});
225+
226+
it('should decrease volume on ArrowDown', () => {
227+
const event = createKeyEvent('ArrowDown', { key: 'ArrowDown' });
228+
keydownHandler(event);
229+
expect(mockPlayer.setVolume).toHaveBeenCalledWith(45);
230+
});
231+
232+
it('should go next on ArrowRight', () => {
233+
const event = createKeyEvent('ArrowRight', { key: 'ArrowRight' });
234+
keydownHandler(event);
235+
expect(mockPlayer.next).toHaveBeenCalled();
236+
});
237+
238+
it('should seek forward on Cmd+ArrowRight', () => {
239+
const event = createKeyEvent('ArrowRight', { key: 'ArrowRight', metaKey: true });
240+
keydownHandler(event);
241+
expect(mockPlayer.seek).toHaveBeenCalledWith(35000);
242+
});
243+
244+
it('should go previous on ArrowLeft', () => {
245+
const event = createKeyEvent('ArrowLeft', { key: 'ArrowLeft' });
246+
keydownHandler(event);
247+
expect(mockPlayer.previous).toHaveBeenCalled();
248+
});
249+
250+
it('should seek back on Cmd+ArrowLeft', () => {
251+
const event = createKeyEvent('ArrowLeft', { key: 'ArrowLeft', metaKey: true });
252+
keydownHandler(event);
253+
expect(mockPlayer.seek).toHaveBeenCalledWith(25000);
254+
});
255+
256+
it('should clamp seek back to 0', () => {
257+
mockPlayer.currentTime = 2000;
258+
const event = createKeyEvent('ArrowLeft', { key: 'ArrowLeft', metaKey: true });
259+
keydownHandler(event);
260+
expect(mockPlayer.seek).toHaveBeenCalledWith(0);
261+
});
262+
});
263+
264+
describe('input suppression', () => {
265+
it('should NOT trigger playback shortcuts when typing in INPUT', () => {
266+
const event = createKeyEvent('Space', {
267+
key: ' ',
268+
target: { tagName: 'INPUT', isContentEditable: false },
269+
});
270+
keydownHandler(event);
271+
expect(mockPlayer.togglePlay).not.toHaveBeenCalled();
272+
});
273+
274+
it('should NOT trigger playback shortcuts when typing in TEXTAREA', () => {
275+
const event = createKeyEvent('ArrowUp', {
276+
key: 'ArrowUp',
277+
target: { tagName: 'TEXTAREA', isContentEditable: false },
278+
});
279+
keydownHandler(event);
280+
expect(mockPlayer.setVolume).not.toHaveBeenCalled();
281+
});
282+
283+
it('should still trigger modifier shortcuts in INPUT fields', () => {
284+
const event = createKeyEvent('KeyM', {
285+
metaKey: true,
286+
shiftKey: true,
287+
target: { tagName: 'INPUT', isContentEditable: false },
288+
});
289+
keydownHandler(event);
290+
expect(mockPlayer.toggleMute).toHaveBeenCalled();
291+
});
292+
});
293+
183294
describe('SHORTCUT_DEFINITIONS', () => {
184295
it('should list mute as requiring modifier+shift', () => {
185296
const muteDef = SHORTCUT_DEFINITIONS.find((s) => s.action === 'Mute / Unmute');

0 commit comments

Comments
 (0)