Skip to content

Commit

Permalink
allow exploding tags
Browse files Browse the repository at this point in the history
  • Loading branch information
dnotes committed Oct 10, 2024
1 parent b3f2ba4 commit 3eacaac
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-crabs-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"quickpickle": patch
---

allow "exploding" tags (tags that make multiple tests for each combination)
42 changes: 42 additions & 0 deletions packages/main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export type QuickPickleConfig<T = {[key:string]:any}> = {
failTags: string|string[]
concurrentTags: string|string[]
sequentialTags: string|string[]
explodeTags: string|string[]|string[][]
worldConfig: T
};

Expand Down Expand Up @@ -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
Expand All @@ -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<QuickPickleConfig> = {}):Plugin => {
let config:QuickPickleConfig
let passedConfig = {...conf}
Expand All @@ -152,6 +192,8 @@ export const quickpickle = (conf:Partial<QuickPickleConfig> = {}):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)) {
Expand Down
7 changes: 6 additions & 1 deletion packages/main/src/render.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {

Expand Down Expand Up @@ -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 = ' ') {
Expand Down
28 changes: 20 additions & 8 deletions packages/main/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
})

})

0 comments on commit 3eacaac

Please sign in to comment.