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']`) }, )