Skip to content

Commit 3e2d036

Browse files
chore(e2e): convert e2e repo from create-react-app to vite-react (#156)
- 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
1 parent e3c3c49 commit 3e2d036

File tree

364 files changed

+7409
-17445
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

364 files changed

+7409
-17445
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ jobs:
1919
uses: bahmutov/npm-install@v1
2020
with:
2121
working-directory: e2e
22+
# e2e repos need the plugin to be built
23+
- run: npm run build
24+
- name: restore dependencies
25+
uses: bahmutov/npm-install@v1
26+
with:
27+
working-directory: e2e/test_repos/repos/tailwindv3/repo
2228
- run: npm run lint:all
29+
- run: npm run lint
30+
working-directory: e2e/test_repos/repos/tailwindv3/repo
2331
type_check:
2432
name: type check
2533
runs-on: ubuntu-latest
@@ -31,7 +39,15 @@ jobs:
3139
node-version: 22
3240
- name: restore dependencies
3341
uses: bahmutov/npm-install@v1
42+
# e2e repos need the plugin to be built
43+
- run: npm run build
44+
- name: restore dependencies
45+
uses: bahmutov/npm-install@v1
46+
with:
47+
working-directory: e2e/test_repos/repos/tailwindv3/repo
48+
- run: npm run type-check
3449
- run: npm run type-check
50+
working-directory: e2e/test_repos/repos/tailwindv3/repo
3551
test:
3652
runs-on: ubuntu-latest
3753
steps:

.prettierignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
node_modules
22
lib
3-
dist
43
coverage

.stylelintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules
22
lib
33
examples
4-
coverage
4+
coverage
5+
dist

e2e/playwright.config.ts

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defineConfig, devices } from '@playwright/test'
2+
import { getRepos } from './test_repos'
23

34
/**
45
* Read environment variables from file.
@@ -35,47 +36,51 @@ export default defineConfig({
3536
},
3637

3738
/* Configure projects for major browsers */
38-
projects: [
39-
// Runs before all other projects to initialize all int tests builds without concurrency problems
40-
{
41-
name: 'chromium',
42-
use: { ...devices['Desktop Chrome'] }
43-
},
44-
45-
{
46-
name: 'firefox',
47-
use: { ...devices['Desktop Firefox'] },
48-
// Counts on the first project to initialize all int test builds to reuse for a performance boost
49-
dependencies: ['chromium']
50-
},
51-
52-
{
53-
name: 'webkit',
54-
use: { ...devices['Desktop Safari'] },
55-
// Counts on the first project to initialize all int test builds to reuse for a performance boost
56-
dependencies: ['chromium']
39+
projects: getRepos().flatMap(repo => {
40+
const initProject = {
41+
name: `chromium - ${repo}`,
42+
use: { ...devices['Desktop Chrome'] },
43+
metadata: { repo }
5744
}
45+
return [
46+
// Runs before all other projects to initialize all int tests builds without concurrency problems
47+
initProject,
48+
{
49+
name: `firefox - ${repo}`,
50+
use: { ...devices['Desktop Firefox'] },
51+
metadata: { repo },
52+
// Counts on the first project to initialize all int test builds to reuse for a performance boost
53+
dependencies: [initProject.name]
54+
},
55+
{
56+
name: `webKit - ${repo}`,
57+
use: { ...devices['Desktop Safari'] },
58+
metadata: { repo },
59+
// Counts on the first project to initialize all int test builds to reuse for a performance boost
60+
dependencies: [initProject.name]
61+
}
5862

59-
/* Test against mobile viewports. */
60-
// {
61-
// name: 'Mobile Chrome',
62-
// use: { ...devices['Pixel 5'] },
63-
// },
64-
// {
65-
// name: 'Mobile Safari',
66-
// use: { ...devices['iPhone 12'] },
67-
// },
63+
/* Test against mobile viewports. */
64+
// {
65+
// name: 'Mobile Chrome',
66+
// use: { ...devices['Pixel 5'] },
67+
// },
68+
// {
69+
// name: 'Mobile Safari',
70+
// use: { ...devices['iPhone 12'] },
71+
// },
6872

69-
/* Test against branded browsers. */
70-
// {
71-
// name: 'Microsoft Edge',
72-
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
73-
// },
74-
// {
75-
// name: 'Google Chrome',
76-
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
77-
// },
78-
]
73+
/* Test against branded browsers. */
74+
// {
75+
// name: 'Microsoft Edge',
76+
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
77+
// },
78+
// {
79+
// name: 'Google Chrome',
80+
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
81+
// },
82+
]
83+
})
7984

8085
/* Run your local dev server before starting the tests */
8186
// webServer: {

e2e/test_repos/drivers.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import path from 'path'
2+
import fse from 'fs-extra'
3+
import { execa } from 'execa'
4+
import { MultiThemePluginOptions } from '@/utils/optionsUtils'
5+
import { type Config as TailwindConfig } from 'tailwindcss'
6+
import {
7+
CommandOptions,
8+
defineRepoInstance,
9+
StartServerOptions,
10+
ServerStarted,
11+
StartServerResult,
12+
StopServerCallback
13+
} from './repo_instance'
14+
import serialize from 'serialize-javascript'
15+
import getPort from 'get-port'
16+
import { getRepoPaths, RepoPaths } from './paths'
17+
18+
export interface OpenOptions {
19+
baseTailwindConfig?: { theme: TailwindConfig['theme'] }
20+
themerConfig: MultiThemePluginOptions
21+
instanceId: string
22+
}
23+
24+
export interface Driver {
25+
install: () => Promise<void>
26+
cleanup: () => Promise<void>
27+
open: (
28+
options: OpenOptions
29+
) => Promise<{ url: string; stop: StopServerCallback }>
30+
}
31+
32+
export const resolveDriver = async (repo: string): Promise<Driver> => {
33+
const repoPaths = getRepoPaths(repo)
34+
try {
35+
const module = (await import(repoPaths.driverFilePath)) as unknown
36+
37+
if (
38+
!module ||
39+
typeof module !== 'object' ||
40+
!('default' in module) ||
41+
!module.default ||
42+
typeof module.default !== 'object'
43+
) {
44+
throw new Error(
45+
`Module ${repoPaths.driverFilePath} does not export a default driver options object`
46+
)
47+
}
48+
49+
return new DriverImpl({
50+
...(module.default as DriverOptions),
51+
repoPaths
52+
})
53+
} catch (error) {
54+
console.error(`Failed to import or use driver for repo: ${repo}`, error)
55+
throw error // Fail the test if the driver fails to load
56+
}
57+
}
58+
59+
export type { StopServerCallback }
60+
61+
export interface DriverOptions {
62+
repoPaths: RepoPaths
63+
installCommand: CommandOptions
64+
getBuildCommand: ({
65+
tailwindConfigFilePath,
66+
buildDirPath
67+
}: {
68+
tailwindConfigFilePath: string
69+
buildDirPath: string
70+
}) => CommandOptions
71+
getStartServerOptions: ({
72+
port,
73+
buildDir
74+
}: {
75+
port: number
76+
buildDir: string
77+
}) => StartServerOptions
78+
}
79+
80+
// Quality of life helper to define driver options
81+
export const defineDriver = <T extends Omit<DriverOptions, 'repoPaths'>>(
82+
options: T
83+
): T => options
84+
85+
class DriverImpl implements Driver {
86+
constructor(private driverOptions: DriverOptions) {}
87+
async install() {
88+
const nodeModulesPath = path.join(
89+
this.driverOptions.repoPaths.repoDirPath,
90+
'node_modules'
91+
)
92+
if (!(await fse.exists(nodeModulesPath))) {
93+
await execa(
94+
this.driverOptions.installCommand.command[0],
95+
this.driverOptions.installCommand.command[1],
96+
{
97+
cwd: this.driverOptions.repoPaths.repoDirPath,
98+
env: this.driverOptions.installCommand.env
99+
}
100+
)
101+
}
102+
}
103+
async cleanup() {
104+
await fse.rm(this.driverOptions.repoPaths.tmpDirPath, {
105+
recursive: true,
106+
force: true
107+
})
108+
}
109+
async open(openOptions: OpenOptions) {
110+
const { instance, isAlreadyInitialized } = await defineRepoInstance({
111+
repoDirPath: this.driverOptions.repoPaths.repoDirPath,
112+
instanceDirPath: path.join(
113+
this.driverOptions.repoPaths.tmpDirPath,
114+
openOptions.instanceId
115+
)
116+
})
117+
118+
if (!isAlreadyInitialized) {
119+
const classesToPreventPurging = this.#parseClasses(
120+
openOptions.themerConfig
121+
)
122+
123+
const tailwindConfig: TailwindConfig = {
124+
content: ['./src/**/*.{js,jsx,ts,tsx}'],
125+
safelist: classesToPreventPurging,
126+
theme: openOptions.baseTailwindConfig?.theme ?? {
127+
extend: {}
128+
}
129+
}
130+
131+
const { filePath: tailwindConfigFilePath } = await instance.writeFile(
132+
'tailwind.config.js',
133+
`module.exports = {
134+
...${JSON.stringify(tailwindConfig)},
135+
plugins: [require('tailwindcss-themer')(${serialize(
136+
openOptions.themerConfig
137+
)})]
138+
}`
139+
)
140+
141+
const buildCommandOptions = this.driverOptions.getBuildCommand({
142+
tailwindConfigFilePath,
143+
buildDirPath: instance.buildDirPath
144+
})
145+
await instance.execute(buildCommandOptions)
146+
}
147+
148+
const { url, stop } = await this.#startServerWithRetry({
149+
maxAttempts: 2,
150+
startServer: async () => {
151+
const port = await getPort()
152+
const startServerOptions = this.driverOptions.getStartServerOptions({
153+
port,
154+
buildDir: instance.buildDirPath
155+
})
156+
return await instance.startServer(startServerOptions)
157+
}
158+
})
159+
160+
return {
161+
url,
162+
stop
163+
}
164+
}
165+
166+
async #startServerWithRetry({
167+
maxAttempts,
168+
startServer
169+
}: {
170+
maxAttempts: number
171+
startServer: () => Promise<StartServerResult>
172+
}): Promise<ServerStarted> {
173+
let attemptNumber = 0
174+
let failedReason = 'unknown'
175+
while (attemptNumber <= maxAttempts) {
176+
attemptNumber++
177+
if (attemptNumber > 1) {
178+
console.log(
179+
`Retrying (attempt ${attemptNumber}) starting the server because: ${failedReason}`
180+
)
181+
}
182+
183+
const result = await startServer()
184+
185+
if (result.started) {
186+
return result
187+
} else {
188+
failedReason = result.reason
189+
}
190+
}
191+
throw new Error(
192+
`Attempted to start server ${attemptNumber} times but couldn't start the server\n\n${failedReason}`
193+
)
194+
}
195+
196+
#parseClasses(config: MultiThemePluginOptions): string[] {
197+
const themeNameClasses = [
198+
'defaultTheme',
199+
...(config.themes?.map(x => x.name) ?? [])
200+
]
201+
// Preventing purging of these styles makes writing tests with arbitrary classes
202+
// easier since otherwise they'd have to define the styles they use when opening
203+
// the repo instance
204+
const stylesToKeep = [
205+
'bg-primary',
206+
'bg-primary/75',
207+
'bg-primary-DEFAULT-500',
208+
'font-title',
209+
'text-textColor',
210+
'text-textColor/50'
211+
]
212+
const preloadedVariantStyles = themeNameClasses.flatMap(themeName =>
213+
stylesToKeep.map(style => `${themeName}:${style}`)
214+
)
215+
const mediaQueries =
216+
config.themes?.map(x => x.mediaQuery ?? '')?.filter(x => !!x) ?? []
217+
const selectors = config.themes?.flatMap(x => x.selectors ?? []) ?? []
218+
return [
219+
...themeNameClasses,
220+
...preloadedVariantStyles,
221+
...mediaQueries,
222+
...selectors,
223+
...stylesToKeep
224+
]
225+
}
226+
}

0 commit comments

Comments
 (0)