Skip to content

Commit

Permalink
Merge pull request #108 from RyanClementsHax/playwright
Browse files Browse the repository at this point in the history
ci(e2e): write tests for config values
  • Loading branch information
RyanClementsHax authored Nov 16, 2023
2 parents 9867ef0 + a989caa commit 5594272
Show file tree
Hide file tree
Showing 95 changed files with 997 additions and 358 deletions.
4 changes: 2 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Config <!-- omit in toc -->

- [Themes named `"dark"`](#themes-named-dark)
- [This plugin's config overwrites what is in the normal tailwind config n collision](#this-plugins-config-overwrites-what-is-in-the-normal-tailwind-config-n-collision)
- [This plugin's config overwrites what is in the normal tailwind config on collision](#this-plugins-config-overwrites-what-is-in-the-normal-tailwind-config-on-collision)
- [Extend](#extend)
- [Valid primitives](#valid-primitives)
- [DEFAULT key](#default-key)
Expand Down Expand Up @@ -77,7 +77,7 @@ Because tailwind automatically adds the `dark` variant for users, defining a the

Therefore, attempting to use `selectors` or `mediaQuery` for a theme named `dark` won't work at all. To prevent users of this plugin from encountering these silent bugs, this plugin crashes when that happens.

## This plugin's config overwrites what is in the normal tailwind config n collision
## This plugin's config overwrites what is in the normal tailwind config on collision

Any config specified in this plugin's config, overwrites what is in the normal tailwind config if there is a collision.

Expand Down
2 changes: 2 additions & 0 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { defineConfig, devices } from '@playwright/test'
*/
export default defineConfig({
testDir: 'tests',
// some tests time out on slower laptops
timeout: 60_000,
testMatch: ['**/*.spec.ts'],
globalSetup: './test_repos/setup.ts',
/* Run tests in files in parallel */
Expand Down
11 changes: 7 additions & 4 deletions e2e/test_repos/drivers/create-react-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import {
import { type Config as TailwindConfig } from 'tailwindcss'

export interface OpenOptions {
baseTailwindConfig?: { theme: TailwindConfig['theme'] }
themerConfig: MultiThemePluginOptions
instanceId: number
titlePath: string[]
}

export async function openWithConfig(
config: MultiThemePluginOptions,
options: OpenOptions
): Promise<{ url: string; stop: StopServerCallback }> {
const tmpDirName = [
Expand All @@ -32,12 +33,12 @@ export async function openWithConfig(
const buildDir = instance.getBuildDir()

if (!isAlreadyInitialized) {
const classesToPreventPurging = parseClasses(config)
const classesToPreventPurging = parseClasses(options.themerConfig)

const tailwindConfig: TailwindConfig = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
safelist: classesToPreventPurging,
theme: {
theme: options.baseTailwindConfig?.theme ?? {
extend: {}
}
}
Expand All @@ -46,7 +47,9 @@ export async function openWithConfig(
'tailwind.config.js',
`module.exports = {
...${JSON.stringify(tailwindConfig)},
plugins: [require('tailwindcss-themer')(${serialize(config)})]
plugins: [require('tailwindcss-themer')(${serialize(
options.themerConfig
)})]
}`
)

Expand Down
11 changes: 6 additions & 5 deletions e2e/test_repos/drivers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export function parseClasses(config: MultiThemePluginOptions): string[] {
...(config.themes?.map(x => x.name) ?? [])
]
const preloadedVariantStyles = themeNameClasses.flatMap(themeName =>
styleVariantsToKeep.map(style => `${themeName}:${style}`)
stylesToKeep.map(style => `${themeName}:${style}`)
)
const mediaQueries =
config.themes?.map(x => x.mediaQuery ?? '')?.filter(x => !!x) ?? []
Expand All @@ -242,11 +242,12 @@ export function parseClasses(config: MultiThemePluginOptions): string[] {
...themeNameClasses,
...preloadedVariantStyles,
...mediaQueries,
...selectors
...selectors,
...stylesToKeep
]
}

// Preventing purging of these styles makes writing variant tests easier
// since otherwise they'd have to define the styles they use when opening
// Preventing purging of these styles makes writing tests with arbitrary classes
// easier since otherwise they'd have to define the styles they use when opening
// the repo instance
const styleVariantsToKeep = ['bg-primary']
const stylesToKeep = ['bg-primary', 'font-title']
124 changes: 98 additions & 26 deletions e2e/test_repos/test.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,117 @@
import { Page, test as base } from '@playwright/test'
import { Page, TestInfo, test as base } from '@playwright/test'
import { MultiThemePluginOptions } from '@/utils/optionsUtils'
import { openWithConfig } from './drivers/create-react-app'
import { StopServerCallback } from './drivers'
import { Config as TailwindConfig } from 'tailwindcss'

export interface TestRepos {
builder(): TestRepoBuilder
}

export interface TestRepoBuilder {
withBaseTailwindConfig(config: {
theme: TailwindConfig['theme']
}): TestRepoBuilder
withThemerConfig(config: MultiThemePluginOptions): TestRepoBuilder
open(): Promise<{ repo: TestRepo; root: ThemeRoot }>
}

export interface TestRepo {
openWithConfig(config: MultiThemePluginOptions): Promise<ThemeRoot>
createRoot(): Promise<ThemeRoot>
}

export interface ThemeRoot {
item: ThemedItem
setClasses(classNames: string[]): Promise<void>
setClass(className: string): Promise<void>
removeClass(className: string): Promise<void>
addClasses(newClasses: string[]): Promise<void>
addClass(newClass: string): Promise<void>
removeClass(classToRemove: string): Promise<void>
setAttribute(key: string, value: string): Promise<void>
createRoot(): Promise<ThemeRoot>
}

export interface ThemedItem {
addClass(newClass: string): Promise<void>
overwriteClassTo(className: string): Promise<void>
}

export const test = base.extend<{ testRepo: TestRepo }>({
testRepo: async ({ page }, use, testInfo) => {
export const test = base.extend<{ testRepos: TestRepos }>({
testRepos: async ({ page }, use, testInfo) => {
const stopCallbacks: StopServerCallback[] = []

const testRepo: TestRepo = {
async openWithConfig(config) {
const { url, stop: _stop } = await openWithConfig(config, {
instanceId: stopCallbacks.length + 1,
titlePath: testInfo.titlePath
})
stopCallbacks.push(_stop)
await page.goto(url)
return this.createRoot()
},
async createRoot() {
await page.getByRole('button', { name: /^add theme root$/i }).click()
const roots = await page.getByTestId(/theme-root-\d/).all()
return new ThemeRootImpl(roots.length.toString(), page)
const testRepos: TestRepos = {
builder() {
return new TestRepoBuilderImpl(
page,
testInfo,
() => stopCallbacks.length + 1,
stop => stopCallbacks.push(stop)
)
}
}

await use(testRepo)
await use(testRepos)

await Promise.all(stopCallbacks.map(x => x()))
await Promise.all(stopCallbacks.map(stop => stop()))
}
})

class TestRepoBuilderImpl implements TestRepoBuilder {
#themerConfig: MultiThemePluginOptions | undefined
#baseTailwindConfig: { theme: TailwindConfig['theme'] } | undefined

constructor(
private readonly page: Page,
private readonly testInfo: TestInfo,
private readonly getInstanceId: () => number,
private readonly registerStopCallback: (stop: StopServerCallback) => void
) {}

withBaseTailwindConfig(config: {
theme: TailwindConfig['theme']
}): TestRepoBuilder {
this.#baseTailwindConfig = config
return this
}

withThemerConfig(themerConfig: MultiThemePluginOptions): TestRepoBuilder {
this.#themerConfig = themerConfig
return this
}

async open(): Promise<{ repo: TestRepo; root: ThemeRoot }> {
if (!this.#themerConfig) {
throw new Error('cannot open without first defining the themer config')
}

const { url, stop: _stop } = await openWithConfig({
instanceId: this.getInstanceId(),
titlePath: this.testInfo.titlePath,
baseTailwindConfig: this.#baseTailwindConfig,
themerConfig: this.#themerConfig
})
this.registerStopCallback(_stop)

await this.page.goto(url)

const repo = new TestRepoImpl(this.page)

return {
repo,
root: await repo.createRoot()
}
}
}

class TestRepoImpl implements TestRepo {
constructor(private readonly page: Page) {}

async createRoot(): Promise<ThemeRoot> {
await this.page.getByRole('button', { name: /^add theme root$/i }).click()
const roots = await this.page.getByTestId(/theme-root-\d/).all()
return new ThemeRootImpl(roots.length.toString(), this.page)
}
}

class ThemeRootImpl implements ThemeRoot {
public item: ThemedItem

Expand All @@ -58,7 +122,7 @@ class ThemeRootImpl implements ThemeRoot {
this.item = new ThemedItemImpl(this.rootId, this.page)
}

async setClasses(newClasses: string[]) {
async addClasses(newClasses: string[]) {
const { className } = await this.#attributes.get()
const classes = (className ?? '').split(' ')
const classesToAdd = newClasses.filter(x => !classes.includes(x))
Expand All @@ -70,8 +134,8 @@ class ThemeRootImpl implements ThemeRoot {
}
}

async setClass(newClass: string) {
await this.setClasses([newClass])
async addClass(newClass: string) {
await this.addClasses([newClass])
}

async removeClass(classToRemove: string) {
Expand Down Expand Up @@ -144,6 +208,14 @@ class ThemedItemImpl implements ThemedItem {
private readonly page: Page
) {}

async addClass(newClass: string): Promise<void> {
const value = await this.#classesInputLocator.inputValue()
const classes = (value ?? '').split(' ')
if (!classes.includes(newClass)) {
await this.overwriteClassTo([...classes, newClass].join(' ').trim())
}
}

async overwriteClassTo(className: string): Promise<void> {
await this.#classesInputLocator.fill(className)
}
Expand Down
Loading

0 comments on commit 5594272

Please sign in to comment.