Skip to content

Commit

Permalink
chore(e2e): convert e2e repo from create-react-app to vite-react (#156)
Browse files Browse the repository at this point in the history
- convert e2e repo's underlying framework from create-react-app to vite-react
- rename test repo to `tailwindv3` in preparation for another repo of `tailwindv4` when the plugin begins to test against that version alongside v3
- refactor e2e repo automation to make it easy to add a new test repo (should just be adding another folder to `test_repos/repos` with a `driver.ts` at its root, and the repo under `repo`)
- ensure test repo is linted and type checked
  • Loading branch information
RyanClementsHax authored Nov 19, 2024
1 parent e3c3c49 commit 3e2d036
Show file tree
Hide file tree
Showing 364 changed files with 7,409 additions and 17,445 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ jobs:
uses: bahmutov/npm-install@v1
with:
working-directory: e2e
# e2e repos need the plugin to be built
- run: npm run build
- name: restore dependencies
uses: bahmutov/npm-install@v1
with:
working-directory: e2e/test_repos/repos/tailwindv3/repo
- run: npm run lint:all
- run: npm run lint
working-directory: e2e/test_repos/repos/tailwindv3/repo
type_check:
name: type check
runs-on: ubuntu-latest
Expand All @@ -31,7 +39,15 @@ jobs:
node-version: 22
- name: restore dependencies
uses: bahmutov/npm-install@v1
# e2e repos need the plugin to be built
- run: npm run build
- name: restore dependencies
uses: bahmutov/npm-install@v1
with:
working-directory: e2e/test_repos/repos/tailwindv3/repo
- run: npm run type-check
- run: npm run type-check
working-directory: e2e/test_repos/repos/tailwindv3/repo
test:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
node_modules
lib
dist
coverage
3 changes: 2 additions & 1 deletion .stylelintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
lib
examples
coverage
coverage
dist
81 changes: 43 additions & 38 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig, devices } from '@playwright/test'
import { getRepos } from './test_repos'

/**
* Read environment variables from file.
Expand Down Expand Up @@ -35,47 +36,51 @@ export default defineConfig({
},

/* Configure projects for major browsers */
projects: [
// Runs before all other projects to initialize all int tests builds without concurrency problems
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
// Counts on the first project to initialize all int test builds to reuse for a performance boost
dependencies: ['chromium']
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
// Counts on the first project to initialize all int test builds to reuse for a performance boost
dependencies: ['chromium']
projects: getRepos().flatMap(repo => {
const initProject = {
name: `chromium - ${repo}`,
use: { ...devices['Desktop Chrome'] },
metadata: { repo }
}
return [
// Runs before all other projects to initialize all int tests builds without concurrency problems
initProject,
{
name: `firefox - ${repo}`,
use: { ...devices['Desktop Firefox'] },
metadata: { repo },
// Counts on the first project to initialize all int test builds to reuse for a performance boost
dependencies: [initProject.name]
},
{
name: `webKit - ${repo}`,
use: { ...devices['Desktop Safari'] },
metadata: { repo },
// Counts on the first project to initialize all int test builds to reuse for a performance boost
dependencies: [initProject.name]
}

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
]
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
]
})

/* Run your local dev server before starting the tests */
// webServer: {
Expand Down
226 changes: 226 additions & 0 deletions e2e/test_repos/drivers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import path from 'path'
import fse from 'fs-extra'
import { execa } from 'execa'
import { MultiThemePluginOptions } from '@/utils/optionsUtils'
import { type Config as TailwindConfig } from 'tailwindcss'
import {
CommandOptions,
defineRepoInstance,
StartServerOptions,
ServerStarted,
StartServerResult,
StopServerCallback
} from './repo_instance'
import serialize from 'serialize-javascript'
import getPort from 'get-port'
import { getRepoPaths, RepoPaths } from './paths'

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

export interface Driver {
install: () => Promise<void>
cleanup: () => Promise<void>
open: (
options: OpenOptions
) => Promise<{ url: string; stop: StopServerCallback }>
}

export const resolveDriver = async (repo: string): Promise<Driver> => {
const repoPaths = getRepoPaths(repo)
try {
const module = (await import(repoPaths.driverFilePath)) as unknown

if (
!module ||
typeof module !== 'object' ||
!('default' in module) ||
!module.default ||
typeof module.default !== 'object'
) {
throw new Error(
`Module ${repoPaths.driverFilePath} does not export a default driver options object`
)
}

return new DriverImpl({
...(module.default as DriverOptions),
repoPaths
})
} catch (error) {
console.error(`Failed to import or use driver for repo: ${repo}`, error)
throw error // Fail the test if the driver fails to load
}
}

export type { StopServerCallback }

export interface DriverOptions {
repoPaths: RepoPaths
installCommand: CommandOptions
getBuildCommand: ({
tailwindConfigFilePath,
buildDirPath
}: {
tailwindConfigFilePath: string
buildDirPath: string
}) => CommandOptions
getStartServerOptions: ({
port,
buildDir
}: {
port: number
buildDir: string
}) => StartServerOptions
}

// Quality of life helper to define driver options
export const defineDriver = <T extends Omit<DriverOptions, 'repoPaths'>>(
options: T
): T => options

class DriverImpl implements Driver {
constructor(private driverOptions: DriverOptions) {}
async install() {
const nodeModulesPath = path.join(
this.driverOptions.repoPaths.repoDirPath,
'node_modules'
)
if (!(await fse.exists(nodeModulesPath))) {
await execa(
this.driverOptions.installCommand.command[0],
this.driverOptions.installCommand.command[1],
{
cwd: this.driverOptions.repoPaths.repoDirPath,
env: this.driverOptions.installCommand.env
}
)
}
}
async cleanup() {
await fse.rm(this.driverOptions.repoPaths.tmpDirPath, {
recursive: true,
force: true
})
}
async open(openOptions: OpenOptions) {
const { instance, isAlreadyInitialized } = await defineRepoInstance({
repoDirPath: this.driverOptions.repoPaths.repoDirPath,
instanceDirPath: path.join(
this.driverOptions.repoPaths.tmpDirPath,
openOptions.instanceId
)
})

if (!isAlreadyInitialized) {
const classesToPreventPurging = this.#parseClasses(
openOptions.themerConfig
)

const tailwindConfig: TailwindConfig = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
safelist: classesToPreventPurging,
theme: openOptions.baseTailwindConfig?.theme ?? {
extend: {}
}
}

const { filePath: tailwindConfigFilePath } = await instance.writeFile(
'tailwind.config.js',
`module.exports = {
...${JSON.stringify(tailwindConfig)},
plugins: [require('tailwindcss-themer')(${serialize(
openOptions.themerConfig
)})]
}`
)

const buildCommandOptions = this.driverOptions.getBuildCommand({
tailwindConfigFilePath,
buildDirPath: instance.buildDirPath
})
await instance.execute(buildCommandOptions)
}

const { url, stop } = await this.#startServerWithRetry({
maxAttempts: 2,
startServer: async () => {
const port = await getPort()
const startServerOptions = this.driverOptions.getStartServerOptions({
port,
buildDir: instance.buildDirPath
})
return await instance.startServer(startServerOptions)
}
})

return {
url,
stop
}
}

async #startServerWithRetry({
maxAttempts,
startServer
}: {
maxAttempts: number
startServer: () => Promise<StartServerResult>
}): Promise<ServerStarted> {
let attemptNumber = 0
let failedReason = 'unknown'
while (attemptNumber <= maxAttempts) {
attemptNumber++
if (attemptNumber > 1) {
console.log(
`Retrying (attempt ${attemptNumber}) starting the server because: ${failedReason}`
)
}

const result = await startServer()

if (result.started) {
return result
} else {
failedReason = result.reason
}
}
throw new Error(
`Attempted to start server ${attemptNumber} times but couldn't start the server\n\n${failedReason}`
)
}

#parseClasses(config: MultiThemePluginOptions): string[] {
const themeNameClasses = [
'defaultTheme',
...(config.themes?.map(x => x.name) ?? [])
]
// 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 stylesToKeep = [
'bg-primary',
'bg-primary/75',
'bg-primary-DEFAULT-500',
'font-title',
'text-textColor',
'text-textColor/50'
]
const preloadedVariantStyles = themeNameClasses.flatMap(themeName =>
stylesToKeep.map(style => `${themeName}:${style}`)
)
const mediaQueries =
config.themes?.map(x => x.mediaQuery ?? '')?.filter(x => !!x) ?? []
const selectors = config.themes?.flatMap(x => x.selectors ?? []) ?? []
return [
...themeNameClasses,
...preloadedVariantStyles,
...mediaQueries,
...selectors,
...stylesToKeep
]
}
}
Loading

0 comments on commit 3e2d036

Please sign in to comment.