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
29 changes: 20 additions & 9 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ on:
- full
- smoke
default: full
grep:
description: Only the tests whose title matches this regular expression (overrides scope)
type: string
default: ''

permissions:
contents: read
Expand Down Expand Up @@ -47,18 +51,27 @@ jobs:
- '!**/*.md'

e2e:
name: E2E (${{ github.event_name == 'pull_request' && 'smoke' || inputs.scope || 'full' }})
name: E2E (${{ github.event_name == 'pull_request' && 'smoke' || inputs.scope || 'full' }}${{ matrix.shards > 1 && format(' {0}/{1}', matrix.shard, matrix.shards) || '' }})
needs: changes
# `changes` is skipped outside pull requests; run regardless of that, but only on a player change in a pull request.
if: |
always() &&
(github.event_name != 'pull_request' || needs.changes.outputs.player == 'true')
runs-on: ubuntu-24.04-arm
timeout-minutes: ${{ github.event_name == 'pull_request' && 30 || 90 }}
# The 4-vCPU runner renders in software: several browsers at once space frames far enough apart that tests pressing
# keys at a time on the page miss their windows or race the end of a short chart (#792). The full run is split over
# four runners with one browser each; the smoke run stays on one runner with four.
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON((github.event_name == 'pull_request' || inputs.scope == 'smoke' || inputs.grep != '' && inputs.grep != null) && '[1]' || '[1, 2, 3, 4]') }}
shards: ${{ fromJSON((github.event_name == 'pull_request' || inputs.scope == 'smoke' || inputs.grep != '' && inputs.grep != null) && '[1]' || '[4]') }}
env:
CI: 'true'
E2E_WORKERS: 4
E2E_GREP: ${{ (github.event_name == 'pull_request' || inputs.scope == 'smoke') && '@smoke' || '' }}
E2E_WORKERS: ${{ (github.event_name == 'pull_request' || inputs.scope == 'smoke') && 4 || 1 }}
E2E_GREP: ${{ inputs.grep || ((github.event_name == 'pull_request' || inputs.scope == 'smoke') && '@smoke' || '') }}
E2E_SHARD: ${{ matrix.shard }}/${{ matrix.shards }}

steps:
- name: Checkout
Expand All @@ -83,17 +96,15 @@ jobs:
shell: bash
run: |
set -euo pipefail
if [ -n "$E2E_GREP" ]; then
pnpm run test:e2e --grep "$E2E_GREP"
else
pnpm run test:e2e
fi
args=(--shard "$E2E_SHARD")
if [ -n "$E2E_GREP" ]; then args+=(--grep "$E2E_GREP"); fi
pnpm run test:e2e "${args[@]}"

- name: Upload report
if: failure() || cancelled()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: e2e-report-${{ github.run_id }}
name: e2e-report-${{ github.run_id }}-${{ matrix.shard }}
path: |
packages/app-play/e2e-report/
packages/app-play/e2e-results/
Expand Down
4 changes: 3 additions & 1 deletion packages/app-play/e2e/bmson-generic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ for (const family of ['native', 'canvas', 'lr2', 'beatoraja'] as const) {
'bound F5 instead of restart',
);
const result = await player.waitForResult(0);
expect(result.results[0]!.judged.pgreat + result.results[0]!.judged.great).toBe(count === 70 ? 5 : 3);
// Every bound key hits its note; a slow CI frame can land a page-timed press in GOOD (#792).
const judged = result.results[0]!.judged;
expect(judged.pgreat + judged.great + judged.good).toBe(count === 70 ? 5 : 3);
expect(result.results[0]!.judged.poor).toBe(0);
const replay = await player.lastReplay();
expect(replay?.chart.laneMode).toBe(`${count} KEY GENERIC`);
Expand Down
4 changes: 3 additions & 1 deletion packages/app-play/e2e/bmson-zero-total.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ for (const family of ['native', 'canvas', 'lr2', 'beatoraja'] as const) {
await player.waitForResult(0);
const replay = await player.lastReplay();
expect(replay?.play.bmsonTotalMode).toBe('spec');
expect((replay?.results?.native?.judge.pgreat ?? 0) + (replay?.results?.native?.judge.great ?? 0)).toBe(2);
// Both notes are hit; a slow CI frame can land a page-timed press in GOOD (#792).
const judge = replay?.results?.native?.judge;
expect((judge?.pgreat ?? 0) + (judge?.great ?? 0) + (judge?.good ?? 0)).toBe(2);
expect(replay?.results?.native?.gauge.final).toBe(20);
if (family === 'native') {
await player.leaveResult();
Expand Down
7 changes: 5 additions & 2 deletions packages/app-play/e2e/chart-text.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ async function textPixels(player: Player): Promise<number> {
for (let y = 0; y < image.height * 0.35; y++)
for (let x = image.width * 0.1; x < image.width * 0.9; x++) {
const offset = (y * image.width + Math.floor(x)) * 4;
if (data[offset]! > 235 && data[offset + 1]! > 235 && data[offset + 2]! > 235) count++;
// Bright, not pure white: Linux renders the thin text with fewer fully white pixels (#792).
if (data[offset]! > 160 && data[offset + 1]! > 160 && data[offset + 2]! > 160) count++;
}
return count;
},
Expand Down Expand Up @@ -45,6 +46,7 @@ for (const family of ['native', 'canvas', 'lr2', 'beatoraja'] as const) {
const before = await textPixels(player);
await player.waitFor((state) => (state.play?.seconds ?? 0) > 2.2, 'text selection');
await expect.poll(() => textPixels(player), { timeout: 2000, intervals: [100] }).toBeGreaterThan(before + 500);
const shown = await textPixels(player);
await player.key('Space');
const paused = (await player.state()).play?.seconds;
await player.page.screenshot({ path: info.outputPath('chart-text.png') });
Expand All @@ -53,7 +55,8 @@ for (const family of ['native', 'canvas', 'lr2', 'beatoraja'] as const) {
expect((await player.state()).play?.seconds).toBeCloseTo(paused!, 2);
await player.key('Space');
await player.waitFor((state) => (state.play?.seconds ?? 0) > 6.2, 'text clear');
await expect.poll(() => textPixels(player), { timeout: 2000, intervals: [100] }).toBeLessThan(before + 300);
// Cleared: back nearer the empty count than the shown one (other HUD text keeps changing).
await expect.poll(() => textPixels(player), { timeout: 2000, intervals: [100] }).toBeLessThan((before + shown) / 2);
if (family === 'native') await expect(player.page.locator('[data-chart-text]')).toBeHidden();
});
}
Expand Down
2 changes: 2 additions & 0 deletions packages/app-play/e2e/course-maker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ async function downloadedText(download: Download): Promise<string> {
}

test('makes, saves, cancels, renames, deletes and exports courses', async ({ player }) => {
// Hundreds of UI steps: the CI runner spends seconds per click (#792).
test.setTimeout(360_000);
const { page } = player;
await openWithLibrary(player);
const card = page.locator('aside.course-maker-card');
Expand Down
Loading