-
Notifications
You must be signed in to change notification settings - Fork 1
feat: allow easy inversion of tests #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -144,6 +144,12 @@ export type ActorTestOptions = Omit<TestOptions, 'retry' | 'timeout'> & { | |
| * @default 60 * 60 * 1000 // 1 hour | ||
| */ | ||
| timeout?: ActorCallOptions['timeout']; | ||
| /** | ||
| * Mark this test as expected to fail (wraps the underlying vitest test with `test.fails`). | ||
| * The test passes as long as it keeps failing, and alerts you (by failing) if it unexpectedly starts passing. | ||
| * Use this to keep a sentinel for known regressions without inverting assertions by hand. | ||
| */ | ||
| fails?: boolean; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
| }; | ||
|
|
||
| declare module 'vitest' { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import type * as Vitest from 'vitest'; | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| // Spy handles: mockFails is called directly as vitestTest.fails(name, opts, fn), | ||
| // mockRunIf is called as vitestTest.runIf(condition) and returns mockRunIfFn. | ||
| const mockRunIfFn = vi.fn(); | ||
| const mockRunIf = vi.fn(() => mockRunIfFn); | ||
| const mockFails = vi.fn(); | ||
|
|
||
| // Mock the vitest module so lib.ts picks up our spies when dynamically imported. | ||
| // We spread the real module so our test file's own `it`, `describe`, `expect` etc. still work. | ||
| // We only replace `test` — `it` remains the real registration function used by this file. | ||
| vi.mock('vitest', async (importOriginal) => { | ||
| const actual = await importOriginal<typeof Vitest>(); | ||
| return { | ||
| ...actual, | ||
| test: Object.assign(vi.fn(), { | ||
| fails: mockFails, | ||
| runIf: mockRunIf, | ||
| }), | ||
| }; | ||
| }); | ||
|
|
||
| const ACTOR_BUILD = { actorName: 'my-actor', actorId: 'abc123', buildNumber: '1.0', buildId: 'build1' }; | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-function | ||
| const noop = () => {}; | ||
|
|
||
| describe('testActor - fails option', () => { | ||
| beforeEach(() => { | ||
| // Fresh module so env vars read at lib.ts module level are re-evaluated each test. | ||
| vi.resetModules(); | ||
| mockRunIf.mockClear(); | ||
| mockRunIfFn.mockClear(); | ||
| mockFails.mockClear(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
|
|
||
| it('calls vitestTest.fails when fails:true and actor is in the build list', async () => { | ||
| vi.stubEnv('ACTOR_BUILDS', JSON.stringify([ACTOR_BUILD])); | ||
|
|
||
| const { testActor } = await import('../../lib/lib.js'); | ||
| testActor('my-actor', 'test name', noop, { fails: true }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a fan of having the options param after the fn but that would need deeper refactor and some type shenanigans to fix. There is already the retry param so this doesn't make it much worse
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's the correct approach, it mirrors vitest parameters nicely |
||
|
|
||
| expect(mockFails).toHaveBeenCalledOnce(); | ||
| expect(mockRunIf).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('calls vitestTest.runIf(false) when fails:true but actor is not in the build list', async () => { | ||
| vi.stubEnv('ACTOR_BUILDS', JSON.stringify([])); | ||
|
|
||
| const { testActor } = await import('../../lib/lib.js'); | ||
| testActor('my-actor', 'test name', noop, { fails: true }); | ||
|
|
||
| expect(mockRunIf).toHaveBeenCalledWith(false); | ||
| expect(mockFails).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('calls vitestTest.runIf(true) when no fails option and actor is in the build list', async () => { | ||
| vi.stubEnv('ACTOR_BUILDS', JSON.stringify([ACTOR_BUILD])); | ||
|
|
||
| const { testActor } = await import('../../lib/lib.js'); | ||
| testActor('my-actor', 'test name', noop); | ||
|
|
||
| expect(mockRunIf).toHaveBeenCalledWith(true); | ||
| expect(mockFails).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('strips the fails key from options forwarded to vitest', async () => { | ||
| vi.stubEnv('ACTOR_BUILDS', JSON.stringify([ACTOR_BUILD])); | ||
|
|
||
| const { testActor } = await import('../../lib/lib.js'); | ||
| testActor('my-actor', 'test name', noop, { fails: true, retry: 2 }); | ||
|
|
||
| const [, options] = mockFails.mock.calls[0] as [string, Record<string, unknown>, unknown]; | ||
| expect(options).not.toHaveProperty('fails'); | ||
| expect(options).toHaveProperty('retry', 2); | ||
| }); | ||
|
|
||
| it('calls vitestTest.fails when fails:true and RUN_ALL_PLATFORM_TESTS is set (no build config)', async () => { | ||
| vi.stubEnv('ACTOR_BUILDS', JSON.stringify([])); | ||
| vi.stubEnv('RUN_ALL_PLATFORM_TESTS', '1'); | ||
|
|
||
| const { testActor } = await import('../../lib/lib.js'); | ||
| testActor('my-actor', 'test name', noop, { fails: true }); | ||
|
|
||
| expect(mockFails).toHaveBeenCalledOnce(); | ||
| expect(mockRunIf).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('testStandbyActor - fails option', () => { | ||
| beforeEach(() => { | ||
| vi.resetModules(); | ||
| mockRunIf.mockClear(); | ||
| mockRunIfFn.mockClear(); | ||
| mockFails.mockClear(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
|
|
||
| it('calls vitestTest.fails when fails:true and actor is in the build list', async () => { | ||
| vi.stubEnv('ACTOR_BUILDS', JSON.stringify([ACTOR_BUILD])); | ||
|
|
||
| const { testStandbyActor } = await import('../../lib/lib.js'); | ||
| testStandbyActor('my-actor', 'test name', noop, { fails: true }); | ||
|
|
||
| expect(mockFails).toHaveBeenCalledOnce(); | ||
| expect(mockRunIf).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems cleaner if it does work