diff --git a/.changeset/nervous-crabs-look.md b/.changeset/nervous-crabs-look.md new file mode 100644 index 0000000..588618e --- /dev/null +++ b/.changeset/nervous-crabs-look.md @@ -0,0 +1,5 @@ +--- +"quickpickle": patch +--- + +allow "exploding" tags (tags that make multiple tests for each combination) diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index f206908..7973bb5 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts @@ -84,6 +84,7 @@ export type QuickPickleConfig = { failTags: string|string[] concurrentTags: string|string[] sequentialTags: string|string[] + explodeTags: string|string[]|string[][] worldConfig: T }; @@ -114,6 +115,11 @@ export const defaultConfig: QuickPickleConfig = { */ sequentialTags: ['@sequential'], + /** + * Explode tags into multiple tests, e.g. for different browsers. + */ + explodeTags: [], + /** * The config for the World class. Must be serializable with JSON.stringify. * Not used by the default World class, but may be used by plugins or custom @@ -133,6 +139,40 @@ export function normalizeTags(tags?:string|string[]|undefined):string[] { return tags.filter(Boolean).map(tag => tag.startsWith('@') ? tag : `@${tag}`) } +function is3d(arr:string|string[]|string[][]):arr is string[][] { + return Array.isArray(arr) && arr.every(item => Array.isArray(item)) +} + +function explodeArray(arr: string[][]): string[][] { + if (arr.length === 0) return [[]]; + + const [first, ...rest] = arr; + const subCombinations = explodeArray(rest); + + return first.flatMap(item => + subCombinations.map(subCombo => [item, ...subCombo]) + ); +} + +export function explodeTags(explodeTags:string[][], testTags:string[]):string[][] { + if (!explodeTags.length) return [testTags] + let tagsToTest = [...testTags] + + // gather a 3d array of items that are shared between tags and each array in explodeTags + // and then remove those items from the tags array + const sharedTags = explodeTags.map(tagList => { + let items = tagList.filter(tag => tagsToTest.includes(tag)) + if (items.length) items.forEach(item => tagsToTest.splice(tagsToTest.indexOf(item), 1)) + return items + }) + + // then, build a 3d array of all possible combinations of the remaining tags + let combined = explodeArray(sharedTags) + + // finally, return the list + return combined.length ? combined.map(arr => [...tagsToTest, ...arr]) : [testTags] +} + export const quickpickle = (conf:Partial = {}):Plugin => { let config:QuickPickleConfig let passedConfig = {...conf} @@ -152,6 +192,8 @@ export const quickpickle = (conf:Partial = {}):Plugin => { config.failTags = normalizeTags(config.failTags) config.concurrentTags = normalizeTags(config.concurrentTags) config.sequentialTags = normalizeTags(config.sequentialTags) + if (is3d(config.explodeTags)) config.explodeTags = config.explodeTags.map(normalizeTags) + else config.explodeTags = [normalizeTags(config.explodeTags)] }, async transform(src: string, id: string) { if (featureRegex.test(id)) { diff --git a/packages/main/src/render.ts b/packages/main/src/render.ts index d4af08e..9ce6225 100644 --- a/packages/main/src/render.ts +++ b/packages/main/src/render.ts @@ -1,5 +1,5 @@ import type { Feature, FeatureChild, GherkinDocument, RuleChild, Step } from "@cucumber/messages"; -import type { QuickPickleConfig } from '.' +import { explodeTags, type QuickPickleConfig } from '.' import * as Gherkin from '@cucumber/gherkin'; import * as Messages from '@cucumber/messages'; @@ -141,6 +141,10 @@ function renderScenario(child:FeatureChild, config:QuickPickleConfig, tags:strin let sequential = (intersection(config.sequentialTags, tags).length > 0) ? '.sequential' : '' let attrs = todo + skip + fails + concurrent + sequential + // Deal with exploding tags + let taglists = explodeTags(config.explodeTags as string[][], tags) + return taglists.map(tags => { + // For Scenario Outlines with examples if (child.scenario!.examples?.[0]?.tableHeader && child.scenario!.examples?.[0]?.tableBody) { @@ -183,6 +187,7 @@ ${renderSteps(child.scenario!.steps as Step[], config, sp + ' ')} ${sp} await afterScenario(state); ${sp}}); ` + }).join('\n\n') } function renderSteps(steps:Step[], config:QuickPickleConfig, sp = ' ') { diff --git a/packages/main/tests/index.test.ts b/packages/main/tests/index.test.ts index adb2e8f..57a143c 100644 --- a/packages/main/tests/index.test.ts +++ b/packages/main/tests/index.test.ts @@ -6,6 +6,7 @@ describe('quickpickle plugin function', async () => { const passedConfig = { skipTags: ['@overwritten-skip'], todoTags: ['@real-todo'], + explodeTags: 'chromium,firefox,webkit', worldConfig: { headless: false, slowMo: 50, @@ -17,34 +18,45 @@ describe('quickpickle plugin function', async () => { } const plugin = quickpickle(passedConfig) - const featureText = ` + // @ts-ignore because we just need to check that the config is resolved in our plugin + plugin.configResolved(viteConfig) + + let feature1 = ` @overwritten-skip @skip Feature: Test Feature @real-todo Scenario: Not skipped, but todo + Given I run the tests @real-skip Scenario: Skipped Given I run the tests ` - - // @ts-ignore because we just need to check that the config is resolved in our plugin - plugin.configResolved(viteConfig) // @ts-ignore - const output = await plugin.transform(featureText, 'test.feature') - console.log(output) - + let output = await plugin.transform(feature1, 'test.feature') test('transform function overwrites default config with passed config, then vite config', () => { expect(output).toContain(`test.todo('Scenario: Not skipped, but todo`) expect(output).toContain(`test.skip('Scenario: Skipped`) expect(output).not.toContain(`describe.skip`) }) - test('transform function writes worldConfig to output', () => { expect(output).toContain(`"slowMo":50`) expect(output).toContain(`"headless":false`) }) + let feature2 = ` +Feature: Exploding Tags + @chromium @firefox @concurrent + Scenario: Multiple browsers + Given I run the tests +` + // @ts-ignore + let output2 = await plugin.transform(feature2, 'test.feature') + test('exploding tags work as expected', () => { + expect(output2).toMatch(/test\.concurrent[\s\S]+?test\.concurrent/m) + expect(output2).not.toMatch(/test\.concurrent[\s\S]+?test\.concurrent[/s/S]+?test\.concurrent/m) + }) + })