From 43fe3735beda0028b58c21236b4a96c960b6d9ef Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Tue, 22 Sep 2026 01:23:13 +0200 Subject: [PATCH] test: mutate the rest of the logic, and close what it found The scope was lib/utils and lib/composables. This adds the entry, the handler registry, the models, the services and the helpers: everything whose behaviour a unit test can actually observe. Components and views stay out, because most of their mutants are DOM glue and the score would measure the wrong thing. Two gaps worth the run, both in code that reads as finished. compareVersions had an assertion for a release outranking its own prerelease, and only in one direction. Nobody ever asked the other way round, and the election asks in whichever order the two copies happen to load, so a beta could have won a page against a release with nothing to show for it. There is now a test both ways, one that checks the comparison is antisymmetric across a spread of versions, and one that sorts a shuffled list. scope.ts went from 70.5% to 82.5%. Every handler answers for a whole selection, and `every` reads exactly like `some` until something asks. Nothing did, so a selection of a score and a PDF would have opened the sheet music viewer. Covered for all four handlers at once. logger.ts is excluded alongside plyrTranslations: its only mutant is the app name it labels lines with, which no test can see. Baseline across the widened scope is 74.6%. mediaPreloader reads as 0% because preloadMedia has no unit test and preloadPreview's arrive with #51. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: skjnldsv --- __tests__/models.spec.ts | 48 ++++++++++++++++++++++++++++++++++++++++ __tests__/scope.spec.ts | 25 +++++++++++++++++++++ stryker.config.json | 9 ++++++-- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/__tests__/models.spec.ts b/__tests__/models.spec.ts index ba0fa2d..5fcb8ec 100644 --- a/__tests__/models.spec.ts +++ b/__tests__/models.spec.ts @@ -156,6 +156,54 @@ describe('audios model', () => { }) }) +/** Register the four built-in handlers, the way each suite below does */ +async function registerAll() { + const [images, videos, audios, sheetmusic] = await Promise.all([ + import('../lib/models/images.ts'), + import('../lib/models/videos.ts'), + import('../lib/models/audios.ts'), + import('../lib/models/sheetmusic.ts'), + ]) + images.registerImageHandler() + videos.registerVideoHandler() + audios.registerAudioHandler() + sheetmusic.registerSheetmusicHandler() +} + +describe('a selection of more than one file', () => { + // Every handler answers for the whole selection, so one file it cannot + // open has to disqualify the lot. Written for all of them at once + // because `every` reads exactly like `some` until something asks. + it.each([ + ['images', 'image/jpeg', 'application/pdf'], + ['videos', 'video/mp4', 'application/pdf'], + ['audios', 'audio/mpeg', 'application/pdf'], + ['sheetmusic', 'application/vnd.recordare.musicxml', 'application/pdf'], + ])('%s refuses a selection it can only partly open', async (id, supported, other) => { + await registerAll() + const handler = handlerById(id) + + expect(handler.enabled([makeFile({ mime: supported })])).toBe(true) + expect(handler.enabled([ + makeFile({ mime: supported }), + makeFile({ mime: other }), + ])).toBe(false) + }) + + it.each([ + ['images', 'image/jpeg', 'image/png'], + ['audios', 'audio/mpeg', 'audio/flac'], + ])('%s takes a selection it can open all of', async (id, first, second) => { + await registerAll() + const handler = handlerById(id) + + expect(handler.enabled([ + makeFile({ mime: first }), + makeFile({ mime: second }), + ])).toBe(true) + }) +}) + describe('sheetmusic model', () => { it.each([ 'application/vnd.recordare.musicxml', diff --git a/__tests__/scope.spec.ts b/__tests__/scope.spec.ts index b199055..e9783da 100644 --- a/__tests__/scope.spec.ts +++ b/__tests__/scope.spec.ts @@ -20,6 +20,31 @@ describe('compareVersions', () => { it('ranks a release above its own prereleases', () => { expect(compareVersions('2.0.0', '2.0.0-beta.1')).toBeGreaterThan(0) + // Both ways round. Only one direction was asserted, and the election + // asks in whichever order the two copies happen to load, so a beta + // could have won a page against a release with nothing to show for it + expect(compareVersions('2.0.0-beta.1', '2.0.0')).toBeLessThan(0) + }) + + it('is the same comparison whichever way it is asked', () => { + // Adding zero, because Math.sign(0) is 0 and negating it is -0, and + // the two are not the same value to a strict comparison + const sign = (value: number) => Math.sign(value) + 0 + const versions = ['1.9.9', '2.0.0-1', '2.0.0-alpha', '2.0.0-beta.2', '2.0.0-beta.10', '2.0.0', '2.0.1'] + for (const a of versions) { + for (const b of versions) { + expect(sign(compareVersions(a, b)), `${a} against ${b}`) + .toBe(-sign(compareVersions(b, a)) + 0) + } + } + }) + + it('orders a full run of versions the way semver does', () => { + // Sorting the list is the question the election actually asks + const ordered = ['1.9.9', '2.0.0-1', '2.0.0-alpha', '2.0.0-beta.2', '2.0.0-beta.10', '2.0.0', '2.0.1'] + const shuffled = ['2.0.0', '2.0.0-beta.10', '1.9.9', '2.0.1', '2.0.0-alpha', '2.0.0-1', '2.0.0-beta.2'] + + expect([...shuffled].sort(compareVersions)).toEqual(ordered) }) it('compares prerelease identifiers the way semver does', () => { diff --git a/stryker.config.json b/stryker.config.json index c0b700f..d3c5468 100644 --- a/stryker.config.json +++ b/stryker.config.json @@ -1,6 +1,6 @@ { "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", - "_comment": "Audits the tests rather than the code: every mutant that survives is a change to the library that no test noticed. Scoped to the pure logic, where it is fast and where a surviving mutant almost always means a real gap rather than an equivalent one.", + "_comment": "Audits the tests rather than the code: a mutant that survives is a change no test noticed. Scoped to the logic, where a survivor usually means a real gap. Components and views are left out: most of their mutants are DOM glue that a unit test cannot observe, so the score would measure the wrong thing.", "packageManager": "npm", "testRunner": "vitest", "inPlace": true, @@ -17,10 +17,15 @@ }, "coverageAnalysis": "perTest", "mutate": [ + "lib/*.ts", "lib/utils/**/*.ts", "lib/composables/**/*.ts", + "lib/models/**/*.ts", + "lib/services/**/*.ts", + "lib/helpers/**/*.ts", "!lib/**/*.d.ts", - "!lib/utils/plyrTranslations.ts" + "!lib/utils/plyrTranslations.ts", + "!lib/services/logger.ts" ], "thresholds": { "high": 90,