diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts
new file mode 100644
index 000000000..c99dc0861
--- /dev/null
+++ b/integrations/postcss/next.test.ts
@@ -0,0 +1,154 @@
+import { expect } from 'vitest'
+import { candidate, css, fetchStylesFromIndex, js, json, retryAssertion, test } from '../utils'
+
+test(
+ 'production build',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "react": "^18",
+ "react-dom": "^18",
+ "next": "^14"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "workspace:^",
+ "tailwindcss": "workspace:^"
+ }
+ }
+ `,
+ 'postcss.config.mjs': js`
+ /** @type {import('postcss-load-config').Config} */
+ const config = {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+ }
+
+ export default config
+ `,
+ 'next.config.mjs': js`
+ /** @type {import('next').NextConfig} */
+ const nextConfig = {}
+
+ export default nextConfig
+ `,
+ 'app/layout.js': js`
+ import './globals.css'
+
+ export default function RootLayout({ children }) {
+ return (
+
+
{children}
+
+ )
+ }
+ `,
+ 'app/page.js': js`
+ export default function Page() {
+ return Hello, Next.js!
+ }
+ `,
+ 'app/globals.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ },
+ },
+ async ({ fs, exec }) => {
+ await exec('pnpm next build')
+
+ let files = await fs.glob('.next/static/css/**/*.css')
+ expect(files).toHaveLength(1)
+ let [filename] = files[0]
+
+ await fs.expectFileToContain(filename, [
+ candidate`underline`,
+ candidate`font-bold`,
+ candidate`text-3xl`,
+ ])
+ },
+)
+;['turbo', 'webpack'].forEach((bundler) => {
+ test(
+ `dev mode (${bundler})`,
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "react": "^18",
+ "react-dom": "^18",
+ "next": "^14"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "workspace:^",
+ "tailwindcss": "workspace:^"
+ }
+ }
+ `,
+ 'postcss.config.mjs': js`
+ /** @type {import('postcss-load-config').Config} */
+ const config = {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+ }
+
+ export default config
+ `,
+ 'next.config.mjs': js`
+ /** @type {import('next').NextConfig} */
+ const nextConfig = {}
+
+ export default nextConfig
+ `,
+ 'app/layout.js': js`
+ import './globals.css'
+
+ export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+ }
+ `,
+ 'app/page.js': js`
+ export default function Page() {
+ return Hello, Next.js!
+ }
+ `,
+ 'app/globals.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ },
+ },
+ async ({ fs, spawn, getFreePort }) => {
+ let port = await getFreePort()
+ await spawn(`pnpm next dev ${bundler === 'turbo' ? '--turbo' : ''} --port ${port}`)
+
+ await retryAssertion(async () => {
+ let css = await fetchStylesFromIndex(port)
+ expect(css).toContain(candidate`underline`)
+ })
+
+ await fs.write(
+ 'app/page.js',
+ js`
+ export default function Page() {
+ return Hello, Next.js!
+ }
+ `,
+ )
+
+ await retryAssertion(async () => {
+ let css = await fetchStylesFromIndex(port)
+ expect(css).toContain(candidate`underline`)
+ expect(css).toContain(candidate`text-red-500`)
+ })
+ },
+ )
+})
diff --git a/integrations/utils.ts b/integrations/utils.ts
index 074f168b9..65b76c960 100644
--- a/integrations/utils.ts
+++ b/integrations/utils.ts
@@ -48,8 +48,14 @@ interface TestFlags {
type SpawnActor = { predicate: (message: string) => boolean; resolve: () => void }
-const TEST_TIMEOUT = 30000
-const ASSERTION_TIMEOUT = 5000
+const IS_WINDOWS = platform() === 'win32'
+
+const TEST_TIMEOUT = IS_WINDOWS ? 120000 : 60000
+const ASSERTION_TIMEOUT = IS_WINDOWS ? 10000 : 5000
+
+// On Windows CI, tmpdir returns a path containing a weird RUNNER~1 folder that
+// apparently causes the vite builds to not work.
+const TMP_ROOT = process.env.CI && IS_WINDOWS ? homedir() : tmpdir()
export function test(
name: string,
@@ -57,17 +63,11 @@ export function test(
testCallback: TestCallback,
{ only = false, debug = false }: TestFlags = {},
) {
- return (only ? defaultTest.only : defaultTest)(
+ return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)(
name,
{ timeout: TEST_TIMEOUT },
async (options) => {
- let rootDir = debug
- ? path.join(REPO_ROOT, '.debug')
- : // On Windows CI, tmpdir returns a path containing a weird RUNNER~1
- // folder that apparently causes the vite builds to not work.
- process.env.CI && platform() === 'win32'
- ? homedir()
- : tmpdir()
+ let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
await fs.mkdir(rootDir, { recursive: true })
let root = await fs.mkdtemp(path.join(rootDir, 'tailwind-integrations'))
@@ -181,7 +181,7 @@ export function test(
})
options.onTestFailed(() => {
- // In debug mode, messages are logged to the console immediatly
+ // In debug mode, messages are logged to the console immediately
if (debug) return
for (let [type, message] of combined) {
@@ -233,7 +233,11 @@ export function test(
return
}
- await killPort(port)
+ try {
+ await killPort(port)
+ } catch {
+ // If the process can not be killed, we can't do anything
+ }
})
resolve(port)
}
@@ -250,7 +254,7 @@ export function test(
}
// Ensure that files written on Windows use \r\n line ending
- if (platform() === 'win32') {
+ if (IS_WINDOWS) {
content = content.replace(/\n/g, '\r\n')
}
@@ -271,7 +275,7 @@ export function test(
)
},
async expectFileToContain(filePath, contents) {
- return retryUntil(async () => {
+ return retryAssertion(async () => {
let fileContent = await this.read(filePath)
for (let content of contents) {
expect(fileContent).toContain(content)
@@ -292,9 +296,11 @@ export function test(
// a separate workspace). We work around this by using the
// `--ignore-workspace` flag.
let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml']
- context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`)
+ await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`)
} catch (error: any) {
console.error(error)
+ console.error(error.stdout?.toString())
+ console.error(error.stderr?.toString())
throw error
}
@@ -303,9 +309,8 @@ export function test(
async function dispose() {
await Promise.all(disposables.map((dispose) => dispose()))
- // Skip removing the directory in CI beause it can stall on Windows
- if (!process.env.CI && !debug) {
- await fs.rm(root, { recursive: true, force: true })
+ if (!debug) {
+ await gracefullyRemove(root)
}
}
@@ -319,7 +324,7 @@ test.only = (name: string, config: TestConfig, testCallback: TestCallback) => {
return test(name, config, testCallback, { only: true })
}
test.debug = (name: string, config: TestConfig, testCallback: TestCallback) => {
- return test(name, config, testCallback, { only: true, debug: true })
+ return test(name, config, testCallback, { debug: true })
}
// Maps package names to their tarball filenames. See scripts/pack-packages.ts
@@ -478,7 +483,7 @@ export function escape(value: string) {
return result
}
-async function retryUntil(
+export async function retryAssertion(
fn: () => Promise,
{ timeout = ASSERTION_TIMEOUT, delay = 5 }: { timeout?: number; delay?: number } = {},
) {
@@ -494,3 +499,50 @@ async function retryUntil(
}
throw error
}
+
+export async function fetchStylesFromIndex(port: number): Promise {
+ let index = await fetch(`http://localhost:${port}`)
+ let html = await index.text()
+
+ let regex = / {
+ let css = await fetch(`http://localhost:${port}${path}`, {
+ headers: {
+ Accept: 'text/css',
+ },
+ })
+ return await css.text()
+ }),
+ )
+
+ return stylesheets.reduce((acc, css) => {
+ return acc + '\n' + css
+ })
+}
+
+async function gracefullyRemove(dir: string) {
+ // Skip removing the directory in CI because it can stall on Windows
+ if (!process.env.CI) {
+ await fs.rm(dir, { recursive: true, force: true })
+ }
+}
+
+async function dirExists(dir: string): Promise {
+ try {
+ return await fs.stat(dir).then((stat) => stat.isDirectory())
+ } catch {
+ return false
+ }
+}
diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts
index ab636eb46..afdd47ab1 100644
--- a/integrations/vite/index.test.ts
+++ b/integrations/vite/index.test.ts
@@ -1,21 +1,6 @@
import path from 'node:path'
import { expect } from 'vitest'
-import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
-
-async function fetchCSS(pathname: string, port: number) {
- // We need to fetch the main index.html file to populate the list of
- // candidates.
- let body = await fetch(`http://localhost:${port}`)
- // Make sure the main request is garbage collected.
- body.blob()
-
- let response = await fetch(`http://localhost:${port}${pathname}`, {
- headers: {
- Accept: 'text/css',
- },
- })
- return response.text()
-}
+import { candidate, css, fetchStylesFromIndex, html, js, json, test, ts, yaml } from '../utils'
test(
'production build',
@@ -140,7 +125,7 @@ test(
await process.onStdout((message) => message.includes('ready in'))
- let css = await fetchCSS('/src/index.css', port)
+ let css = await fetchStylesFromIndex(port)
expect(css).toContain(candidate`underline`)
await fs.write(
@@ -156,7 +141,7 @@ test(
)
await process.onStdout((message) => message.includes('page reload'))
- css = await fetchCSS('/src/index.css', port)
+ css = await fetchStylesFromIndex(port)
expect(css).toContain(candidate`m-2`)
await fs.write(
@@ -168,7 +153,7 @@ test(
)
await process.onStdout((message) => message.includes('page reload'))
- css = await fetchCSS('/src/index.css', port)
+ css = await fetchStylesFromIndex(port)
expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
},
)