Skip to content

Commit

Permalink
feat(playwright): Initial release
Browse files Browse the repository at this point in the history
feat: added worldConfig to QuickPickleConfig, for config passed to World constructor
chore: added vitest config to root for playwright workspace
fix: added re-export of QuickPickleWorld and QuickPickleWorldInterface types
feat: World constructor classes can now accept three params for the constructor:

1. context: the vitest test context
2. info: the gherkin info for the step
3. config: the worldConfig object of QuickPickleConfig
  • Loading branch information
dnotes committed Oct 9, 2024
1 parent df449a4 commit 8f5f661
Show file tree
Hide file tree
Showing 21 changed files with 511 additions and 236 deletions.
6 changes: 6 additions & 0 deletions .changeset/tough-gifts-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"quickpickle": minor
"@quickpickle/playwright": patch
---

Release playwright extension, and many fixes to make it work.
2 changes: 1 addition & 1 deletion packages/main/gherkin-example/example.feature.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions packages/main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { renderGherkin } from './render';
import { DataTable } from '@cucumber/cucumber';
import { DocString } from './models/DocString';

export { setWorldConstructor, getWorldConstructor } from './world';
export { setWorldConstructor, getWorldConstructor, QuickPickleWorld, QuickPickleWorldInterface } from './world';
export { DocString, DataTable }

const featureRegex = /\.feature(?:\.md)?$/;
Expand Down Expand Up @@ -84,6 +84,7 @@ export type QuickPickleConfig = {
failTags: string|string[]
concurrentTags: string|string[]
sequentialTags: string|string[]
worldConfig: {[key:string]:any}
};

export const defaultConfig: QuickPickleConfig = {
Expand All @@ -101,7 +102,7 @@ export const defaultConfig: QuickPickleConfig = {
/**
* Tags to mark as failing, using Vitest's `test.failing` implementation.
*/
failTags: ['@fails'],
failTags: ['@fails', '@failing'],

/**
* Tags to run in parallel, using Vitest's `test.concurrent` implementation.
Expand All @@ -113,6 +114,13 @@ export const defaultConfig: QuickPickleConfig = {
*/
sequentialTags: ['@sequential'],

/**
* 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
* implementations, like @quickpickle/playwright.
*/
worldConfig: {}

}

interface ResolvedConfig {
Expand All @@ -121,7 +129,8 @@ interface ResolvedConfig {
};
}

export function normalizeTags(tags:string|string[]):string[] {
export function normalizeTags(tags?:string|string[]|undefined):string[] {
if (!tags) return []
tags = Array.isArray(tags) ? tags : tags.split(/\s*,\s*/g)
return tags.filter(Boolean).map(tag => tag.startsWith('@') ? tag : `@${tag}`)
}
Expand Down
6 changes: 4 additions & 2 deletions packages/main/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ export function renderFeature(feature:Feature, config:QuickPickleConfig) {
// Get the background stes and all the scenarios
let { backgroundSteps, children } = renderChildren(feature.children as FeatureChild[], config, tags)

let featureName = `${q(feature.keyword)}: ${q(feature.name)}`

// Render the initScenario function, which will be called at the beginning of each scenario
return`
const initScenario = async(context, scenario, tags) => {
let state = new World(context);
let state = new World(context, { feature:'${featureName}', scenario, tags }, ${JSON.stringify(config.worldConfig || {})});
await state.init();
state.common = common;
state.info.feature = '${q(feature.keyword)}: ${q(feature.name)}';
state.info.feature = '${featureName}';
state.info.scenario = scenario;
state.info.tags = [...tags];
await applyBeforeHooks(state);
Expand Down
15 changes: 10 additions & 5 deletions packages/main/src/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,28 @@ export class QuickPickleWorld implements QuickPickleWorldInterface {
feature: '',
scenario: '',
tags: [],
rule: '',
step: '',
}
common: QuickPickleWorldInterface['common'] = {}
context: TestContext
constructor(context:TestContext) {
constructor(context:TestContext, info?:QuickPickleWorldInterface['info']) {
this.context = context
if (info) this.info = {...info}
}
async init() {}
}

let worldConstructor = QuickPickleWorld
export type WorldConstructor = new (
context: TestContext,
info?: QuickPickleWorldInterface['info'],
worldConfig?: any
) => QuickPickleWorldInterface;

let worldConstructor:WorldConstructor = QuickPickleWorld

export function getWorldConstructor() {
return worldConstructor
}

export function setWorldConstructor(constructor: new (context:TestContext) => QuickPickleWorldInterface) {
export function setWorldConstructor(constructor: WorldConstructor) {
worldConstructor = constructor
}
74 changes: 74 additions & 0 deletions packages/playwright/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"name": "@quickpickle/playwright",
"version": "0.9.0",
"description": "Support files for running tests with Playwright using QuickPickle (Gherkin in Vitest).",
"keywords": [
"BDD",
"testing",
"behavioral",
"cucumber",
"gherkin",
"vitest",
"playwright",
"react",
"svelte",
"vue",
"angular"
],
"homepage": "https://github.com/dnotes/quickpickle#readme",
"bugs": {
"url": "https://github.com/dnotes/quickpickle/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/dnotes/quickpickle.git"
},
"license": "MIT",
"type": "module",
"main": "./dist/PlaywrightWorld.cjs",
"module": "./dist/PlaywrightWorld.esm.js",
"types": "./dist/PlaywrightWorld.d.ts",
"exports": {
".": {
"require": "./dist/world.cjs",
"import": "./dist/world.esm.js",
"types": "./dist/world.d.ts"
},
"./actions": {
"require": "./dist/actions.steps.cjs",
"import": "./dist/actions.steps.esm.js",
"types": "./dist/actions.steps.d.ts"
},
"./outcomes": {
"require": "./dist/outcomes.steps.cjs",
"import": "./dist/outcomes.steps.esm.js",
"types": "./dist/outcomes.steps.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "rollup -c",
"type-check": "tsc --noEmit",
"test:watch": "vitest",
"test": "vitest --run"
},
"author": "David Hunt",
"dependencies": {
"@playwright/test": "^1.48.0",
"lodash-es": "^4.17.21",
"playwright": "^1.48.0",
"quickpickle": "workspace:^",
"vite": "^5.0.11"
},
"devDependencies": {
"@rollup/plugin-replace": "^6.0.1",
"@rollup/plugin-typescript": "^12.1.0",
"@types/lodash-es": "^4.17.12",
"fast-glob": "^3.3.2",
"rollup": "^3.20.7",
"typescript": "^5.6.2",
"vitest": "^2.1.2"
}
}
54 changes: 54 additions & 0 deletions packages/playwright/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import typescript from '@rollup/plugin-typescript';
import replace from '@rollup/plugin-replace';
import glob from 'fast-glob';
import path from 'node:path';

const input = Object.fromEntries(
glob.sync('src/**/*.ts').map(file => [
// This will remove `src/` from the beginning and `.ts` from the end
path.relative('src', file.slice(0, -3)),
file
])
);

export default {
input,
output: [
{
dir: 'dist',
format: 'cjs',
sourcemap: true,
exports: 'named',
entryFileNames: '[name].cjs'
},
{
dir: 'dist',
format: 'esm',
sourcemap: true,
exports: 'named',
entryFileNames: '[name].esm.js'
}
],
plugins: [
replace({
preventAssignment: true,
values: {
'import.meta?.env?.MODE': JSON.stringify('production'),
'process?.env?.NODE_ENV': JSON.stringify('production'),
}
}),
typescript({
tsconfig: './tsconfig.json',
declaration: true,
}),
],
external: [
'@playwright/test',
'playwright',
'quickpickle',
'vite',
'node:path',
'node:url',
'lodash-es',
]
};
59 changes: 59 additions & 0 deletions packages/playwright/src/PlaywrightWorld.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
import { normalizeTags, QuickPickleWorld, QuickPickleWorldInterface } from 'quickpickle';
import { After } from 'quickpickle';
import type { TestContext } from 'vitest';
import { defaults, intersection } from 'lodash-es'

export type PlaywrightWorldConfigSetting = {
nojsTags?: string|string[]
headless: boolean
sloMo: number
}

export const defaultPlaywrightWorldConfig = {
nojsTags: ['@nojs', '@noscript'],
headless: true,
slowMo: 0,
}

export type PlaywrightWorldConfig = typeof defaultPlaywrightWorldConfig

export class PlaywrightWorld extends QuickPickleWorld {
browser!: Browser
browserContext!: BrowserContext
page!: Page
playwrightConfig:PlaywrightWorldConfig = defaultPlaywrightWorldConfig

constructor(context:TestContext, info:QuickPickleWorldInterface['info']|undefined, worldConfig?:PlaywrightWorldConfigSetting) {
super(context, info)
let newConfig = defaults(defaultPlaywrightWorldConfig, worldConfig || {})
newConfig.nojsTags = normalizeTags(newConfig.nojsTags)
this.playwrightConfig = newConfig
}

async init() {
this.browser = await chromium.launch()
this.browserContext = await this.browser.newContext({
serviceWorkers: 'block',
javaScriptEnabled: intersection(this.info.tags, this.playwrightConfig.nojsTags)?.length ? false : true,
})
this.page = await this.browserContext.newPage()
}

async reset() {
await this.page?.close()
await this.browserContext?.close()
this.browserContext = await this.browser.newContext({
serviceWorkers: 'block'
})
this.page = await this.browserContext.newPage()
}

async close() {
await this.browser.close()
}
}

After(async (world:PlaywrightWorld) => {
await world.browserContext.close()
})
Loading

0 comments on commit 8f5f661

Please sign in to comment.