diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index b42111d..9a3a836 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -10,6 +10,9 @@ import { } from '../' import {outcomeWithName} from './utils' +// The tests run on Node, but the project has no `@types/node`. +declare const process: {env: Record} + describe('basic functionality', () => { describe('transform value', () => { it('can parse Numbers `+foo -> {foo: Number($value)}`', () => { @@ -28,6 +31,14 @@ describe('basic functionality', () => { foo: false, }) }) + it('can parse true Booleans `&foo: true -> {foo: true}`', () => { + const formData = new FormData() + formData.append('&foo', 'true') + const result = parseFormData(formData) + expect(result).toEqual({ + foo: true, + }) + }) it("can parse checkbox Booleans `&foo: on -> {foo: $value === 'on'}`", () => { const formData = new FormData() formData.append('&foo', 'on') @@ -801,7 +812,10 @@ describe('bracket keys', () => { // Each shape needs the matching `]` of every `[`. Searching for it again // for every `[` takes seconds at this length. - it.each([ + // Skipped during mutation testing: Stryker's instrumented code runs ~4x + // slower on all CPU cores at once, so these tests fail without a mutant and + // "kill" whichever mutant is active. + it.skipIf(process.env['STRYKER_MUTATOR_WORKER'] !== undefined).each([ ['a[b]', 'x'], ['a[', ''], ['a[0]b', ''], @@ -1013,6 +1027,36 @@ describe('Object.prototype names', () => { expect(result).toStrictEqual({}) }) + // A removed entry can go through an empty object or array that a kept entry + // set (only possible with such a transform). Undoing the removed entry must + // not delete it. + it.each<[string, unknown, string, unknown]>([ + ['a', {}, 'a.toString', {a: {}}], + ['a', [], 'a[].toString', {a: []}], + ])( + 'keeps %s with the value %j when %s is removed', + (keptPath, keptValue, removedPath, expected) => { + const values = new Map([ + [keptPath, keptValue], + [removedPath, 'x'], + ]) + const result = parseFormData( + [ + [keptPath, 'x'], + [removedPath, 'x'], + ], + { + transformEntry: ([path]) => ({ + path, + // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- see above + value: values.get(path) as string, + }), + }, + ) + expect(result).toStrictEqual(expected) + }, + ) + // The same errors as for an unused name, so an error doesn't tell the // client which names exist on Object.prototype. it.each([ @@ -1090,6 +1134,29 @@ describe('Object.prototype names', () => { expect(parseFormData([['a.patched', '1']])).toStrictEqual({}) }) }) + + // Only object path parts are compared with Object.prototype. `a[]` has no + // object key, so its entries stay. + describe('an empty-string property added to Object.prototype', () => { + const proto: object = Object.prototype + afterEach(() => { + Reflect.deleteProperty(proto, '') + }) + + it('keeps the entries of `a[]`', () => { + Object.defineProperty(proto, '', { + value: 'x', + configurable: true, + writable: true, + }) + expect( + parseFormData([ + ['a[]', 'x'], + ['a[]', 'y'], + ]), + ).toStrictEqual({a: ['x', 'y']}) + }) + }) }) class RemixFileSubclass extends File {} diff --git a/stryker.config.mjs b/stryker.config.mjs index d25d811..e6f8da0 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -5,10 +5,13 @@ const config = { testRunner: 'vitest', mutate: ['src/index.ts', 'src/path-parts.ts', 'src/transform.ts'], reporters: ['html', 'clear-text', 'progress'], - // The first runs scored 95.0-97.0%. The score moves because the tests that - // parse 200 000-character paths in under 500 ms sometimes fail under - // Stryker's load and so kill a mutant. `break` leaves room for that. - thresholds: {high: 97, low: 95, break: 93}, + // The score is 96.37% (292 of 303 mutants) in every run. The 11 surviving + // mutants are equivalent: they can't change any output. `// Stryker + // disable` comments can't exclude them, because a comment disables every + // mutant of that mutator on the line, including ones the tests kill. + // `break` is just under the score, so one new surviving mutant + // (291 of 303 = 96.04%) fails the run. Raise it when the score goes up. + thresholds: {high: 100, low: 95, break: 96.3}, // Stryker rewrites `extends`/`references` paths in this file with the // TypeScript JS API, which TypeScript 7 doesn't ship. Our tsconfig.json has // neither, so we point Stryker at a file that doesn't exist and it skips