adding Tailwind E2E tests with Playwright
This commit is contained in:
parent
65b448b409
commit
5b506b1260
11 changed files with 388 additions and 2 deletions
12
packages/astro/e2e/fixtures/tailwindcss/astro.config.mjs
Normal file
12
packages/astro/e2e/fixtures/tailwindcss/astro.config.mjs
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [tailwind()],
|
||||
vite: {
|
||||
build: {
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
},
|
||||
});
|
9
packages/astro/e2e/fixtures/tailwindcss/package.json
Normal file
9
packages/astro/e2e/fixtures/tailwindcss/package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/e2e-tailwindcss",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/tailwind": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
const path = require('path');
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: path.join(__dirname, 'tailwind.config.js'), // update this if your path differs!
|
||||
},
|
||||
autoprefixer: {}
|
||||
},
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
let { type = 'button' } = Astro.props;
|
||||
---
|
||||
|
||||
<button
|
||||
class="py-2 px-4 lg:py-3 lg:px-5 bg-purple-600 text-white font-[900] rounded-lg shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-opacity-75"
|
||||
{type}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
|
@ -0,0 +1 @@
|
|||
<div id="complex" class="w-10/12 2xl:w-[80%]"></div>
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
// Component Imports
|
||||
import Button from '../components/Button.astro';
|
||||
import Complex from '../components/Complex.astro';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<title>Astro + TailwindCSS</title>
|
||||
</head>
|
||||
|
||||
<body class="bg-dawn text-midnight">
|
||||
<Button>I’m a Tailwind Button!</Button>
|
||||
<Complex />
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "Markdown + Tailwind"
|
||||
setup: |
|
||||
import Button from '../components/Button.astro';
|
||||
import Complex from '../components/Complex.astro';
|
||||
---
|
||||
|
||||
<div class="grid place-items-center h-screen content-center">
|
||||
<Button>Tailwind Button in Markdown!</Button>
|
||||
<Complex />
|
||||
</div>
|
14
packages/astro/e2e/fixtures/tailwindcss/tailwind.config.js
Normal file
14
packages/astro/e2e/fixtures/tailwindcss/tailwind.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
content: [path.join(__dirname, 'src/**/*.{astro,html,js,jsx,md,svelte,ts,tsx,vue}')],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dawn: '#f3e9fa',
|
||||
dusk: '#514375',
|
||||
midnight: '#31274a',
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
98
packages/astro/e2e/tailwindcss.test.js
Normal file
98
packages/astro/e2e/tailwindcss.test.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { test as base, expect } from '@playwright/test';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const test = base.extend({
|
||||
astro: async ({ }, use) => {
|
||||
const fixture = await loadFixture({ root: './fixtures/tailwindcss/' });
|
||||
await use(fixture);
|
||||
},
|
||||
});
|
||||
|
||||
test.describe('dev', () => {
|
||||
let devServer;
|
||||
|
||||
test.beforeAll(async ({ astro }) => {
|
||||
devServer = await astro.startDevServer();
|
||||
});
|
||||
|
||||
test.afterAll(async ({ astro }) => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
test('Tailwind CSS', async ({ page }) => {
|
||||
await page.goto(`localhost:${devServer.address.port}/`);
|
||||
|
||||
await test.step('body', async () => {
|
||||
const body = page.locator('body');
|
||||
|
||||
await expect(body, 'should have classes').toHaveClass('bg-dawn text-midnight');
|
||||
await expect(body, 'should have background color').toHaveCSS(
|
||||
'background-color',
|
||||
'rgb(243, 233, 250)'
|
||||
);
|
||||
await expect(body, 'should have color').toHaveCSS('color', 'rgb(49, 39, 74)');
|
||||
});
|
||||
|
||||
await test.step('button', async () => {
|
||||
const button = page.locator('button');
|
||||
|
||||
await expect(button, 'should have bg-purple-600').toHaveClass(/bg-purple-600/);
|
||||
await expect(button, 'should have background color').toHaveCSS(
|
||||
'background-color',
|
||||
'rgb(147, 51, 234)'
|
||||
);
|
||||
|
||||
await expect(button, 'should have lg:py-3').toHaveClass(/lg:py-3/);
|
||||
await expect(button, 'should have padding bottom').toHaveCSS('padding-bottom', '12px');
|
||||
await expect(button, 'should have padding top').toHaveCSS('padding-top', '12px');
|
||||
|
||||
await expect(button, 'should have font-[900]').toHaveClass(/font-\[900\]/);
|
||||
await expect(button, 'should have font weight').toHaveCSS('font-weight', '900');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('build', () => {
|
||||
let previewServer;
|
||||
|
||||
test.beforeAll(async ({ astro }) => {
|
||||
await astro.build();
|
||||
previewServer = await astro.preview();
|
||||
});
|
||||
|
||||
test.afterAll(async ({ astro }) => {
|
||||
await previewServer.stop();
|
||||
})
|
||||
|
||||
test('Tailwind CSS', async ({ page }) => {
|
||||
await page.goto(`localhost:3000/`);
|
||||
|
||||
await test.step('body', async () => {
|
||||
const body = page.locator('body');
|
||||
|
||||
await expect(body, 'should have classes').toHaveClass('bg-dawn text-midnight');
|
||||
await expect(body, 'should have background color').toHaveCSS(
|
||||
'background-color',
|
||||
'rgb(243, 233, 250)'
|
||||
);
|
||||
await expect(body, 'should have color').toHaveCSS('color', 'rgb(49, 39, 74)');
|
||||
});
|
||||
|
||||
await test.step('button', async () => {
|
||||
const button = page.locator('button');
|
||||
|
||||
await expect(button, 'should have bg-purple-600').toHaveClass(/bg-purple-600/);
|
||||
await expect(button, 'should have background color').toHaveCSS(
|
||||
'background-color',
|
||||
'rgb(147, 51, 234)'
|
||||
);
|
||||
|
||||
await expect(button, 'should have lg:py-3').toHaveClass(/lg:py-3/);
|
||||
await expect(button, 'should have padding bottom').toHaveCSS('padding-bottom', '12px');
|
||||
await expect(button, 'should have padding top').toHaveCSS('padding-top', '12px');
|
||||
|
||||
await expect(button, 'should have font-[900]').toHaveClass(/font-\[900\]/);
|
||||
await expect(button, 'should have font weight').toHaveCSS('font-weight', '900');
|
||||
});
|
||||
});
|
||||
});
|
202
packages/astro/e2e/test-utils.js
Normal file
202
packages/astro/e2e/test-utils.js
Normal file
|
@ -0,0 +1,202 @@
|
|||
import { execa } from 'execa';
|
||||
import { polyfill } from '@astrojs/webapi';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { resolveConfig, loadConfig } from '../dist/core/config.js';
|
||||
import dev from '../dist/core/dev/index.js';
|
||||
import build from '../dist/core/build/index.js';
|
||||
import preview from '../dist/core/preview/index.js';
|
||||
import { nodeLogDestination } from '../dist/core/logger/node.js';
|
||||
import os from 'os';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
// polyfill WebAPIs to globalThis for Node v12, Node v14, and Node v16
|
||||
polyfill(globalThis, {
|
||||
exclude: 'window document',
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import('node-fetch').Response} Response
|
||||
* @typedef {import('../src/core/dev/index').DevServer} DevServer
|
||||
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
|
||||
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
|
||||
* @typedef {import('../src/core/app/index').App} App
|
||||
*
|
||||
*
|
||||
* @typedef {Object} Fixture
|
||||
* @property {typeof build} build
|
||||
* @property {(url: string, opts: any) => Promise<Response>} fetch
|
||||
* @property {(path: string) => Promise<string>} readFile
|
||||
* @property {(path: string) => Promise<string[]>} readdir
|
||||
* @property {() => Promise<DevServer>} startDevServer
|
||||
* @property {() => Promise<PreviewServer>} preview
|
||||
* @property {() => Promise<void>} clean
|
||||
* @property {() => Promise<App>} loadTestAdapterApp
|
||||
*/
|
||||
|
||||
/**
|
||||
* Load Astro fixture
|
||||
* @param {AstroConfig} inlineConfig Astro config partial (note: must specify `root`)
|
||||
* @returns {Promise<Fixture>} The fixture. Has the following properties:
|
||||
* .config - Returns the final config. Will be automatically passed to the methods below:
|
||||
*
|
||||
* Build
|
||||
* .build() - Async. Builds into current folder (will erase previous build)
|
||||
* .readFile(path) - Async. Read a file from the build.
|
||||
*
|
||||
* Dev
|
||||
* .startDevServer() - Async. Starts a dev server at an available port. Be sure to call devServer.stop() before test exit.
|
||||
* .fetch(url) - Async. Returns a URL from the prevew server (must have called .preview() before)
|
||||
*
|
||||
* Preview
|
||||
* .preview() - Async. Starts a preview server. Note this can’t be running in same fixture as .dev() as they share ports. Also, you must call `server.close()` before test exit
|
||||
*
|
||||
* Clean-up
|
||||
* .clean() - Async. Removes the project’s dist folder.
|
||||
*/
|
||||
export async function loadFixture(inlineConfig) {
|
||||
if (!inlineConfig || !inlineConfig.root)
|
||||
throw new Error("Must provide { root: './fixtures/...' }");
|
||||
|
||||
// load config
|
||||
let cwd = inlineConfig.root;
|
||||
delete inlineConfig.root;
|
||||
if (typeof cwd === 'string') {
|
||||
try {
|
||||
cwd = new URL(cwd.replace(/\/?$/, '/'));
|
||||
} catch (err1) {
|
||||
cwd = new URL(cwd.replace(/\/?$/, '/'), import.meta.url);
|
||||
}
|
||||
}
|
||||
// Load the config.
|
||||
let config = await loadConfig({ cwd: fileURLToPath(cwd) });
|
||||
config = merge(config, { ...inlineConfig, root: cwd });
|
||||
|
||||
// Note: the inline config doesn't run through config validation where these normalizations usually occur
|
||||
if (typeof inlineConfig.site === 'string') {
|
||||
config.site = new URL(inlineConfig.site);
|
||||
}
|
||||
if (inlineConfig.base && !inlineConfig.base.endsWith('/')) {
|
||||
config.base = inlineConfig.base + '/';
|
||||
}
|
||||
|
||||
/** @type {import('../src/core/logger/core').LogOptions} */
|
||||
const logging = {
|
||||
dest: nodeLogDestination,
|
||||
level: 'error',
|
||||
};
|
||||
|
||||
/** @type {import('@astrojs/telemetry').AstroTelemetry} */
|
||||
const telemetry = {
|
||||
record() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
build: (opts = {}) => build(config, { mode: 'development', logging, telemetry, ...opts }),
|
||||
startDevServer: async (opts = {}) => {
|
||||
const devResult = await dev(config, { logging, telemetry, ...opts });
|
||||
config.server.port = devResult.address.port; // update port
|
||||
return devResult;
|
||||
},
|
||||
config,
|
||||
fetch: (url, init) =>
|
||||
fetch(`http://${'127.0.0.1'}:${config.server.port}${url.replace(/^\/?/, '/')}`, init),
|
||||
preview: async (opts = {}) => {
|
||||
const previewServer = await preview(config, { logging, telemetry, ...opts });
|
||||
return previewServer;
|
||||
},
|
||||
readFile: (filePath) =>
|
||||
fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.outDir), 'utf8'),
|
||||
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.outDir)),
|
||||
clean: () => fs.promises.rm(config.outDir, { maxRetries: 10, recursive: true, force: true }),
|
||||
loadTestAdapterApp: async () => {
|
||||
const url = new URL('./server/entry.mjs', config.outDir);
|
||||
const { createApp } = await import(url);
|
||||
return createApp();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic object merge utility. Returns new copy of merged Object.
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @returns {Object}
|
||||
*/
|
||||
function merge(a, b) {
|
||||
const allKeys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
||||
const c = {};
|
||||
for (const k of allKeys) {
|
||||
const needsObjectMerge =
|
||||
typeof a[k] === 'object' &&
|
||||
typeof b[k] === 'object' &&
|
||||
(Object.keys(a[k]).length || Object.keys(b[k]).length) &&
|
||||
!Array.isArray(a[k]) &&
|
||||
!Array.isArray(b[k]);
|
||||
if (needsObjectMerge) {
|
||||
c[k] = merge(a[k] || {}, b[k] || {});
|
||||
continue;
|
||||
}
|
||||
c[k] = a[k];
|
||||
if (b[k] !== undefined) c[k] = b[k];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
const cliPath = fileURLToPath(new URL('../astro.js', import.meta.url));
|
||||
|
||||
/** Returns a process running the Astro CLI. */
|
||||
export function cli(/** @type {string[]} */ ...args) {
|
||||
const spawned = execa('node', [cliPath, ...args]);
|
||||
|
||||
spawned.stdout.setEncoding('utf8');
|
||||
|
||||
return spawned;
|
||||
}
|
||||
|
||||
export async function parseCliDevStart(proc) {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
for await (const chunk of proc.stdout) {
|
||||
stdout += chunk;
|
||||
if (chunk.includes('Local')) break;
|
||||
}
|
||||
if (!stdout) {
|
||||
for await (const chunk of proc.stderr) {
|
||||
stderr += chunk;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
stdout = stripAnsi(stdout);
|
||||
stderr = stripAnsi(stderr);
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
const messages = stdout
|
||||
.split('\n')
|
||||
.filter((ln) => !!ln.trim())
|
||||
.map((ln) => ln.replace(/[🚀┃]/g, '').replace(/\s+/g, ' ').trim());
|
||||
|
||||
return { messages };
|
||||
}
|
||||
|
||||
export async function cliServerLogSetup(flags = [], cmd = 'dev') {
|
||||
const proc = cli(cmd, ...flags);
|
||||
|
||||
const { messages } = await parseCliDevStart(proc);
|
||||
|
||||
const local = messages.find((msg) => msg.includes('Local'))?.replace(/Local\s*/g, '');
|
||||
const network = messages.find((msg) => msg.includes('Network'))?.replace(/Network\s*/g, '');
|
||||
|
||||
return { local, network };
|
||||
}
|
||||
|
||||
export const isWindows = os.platform() === 'win32';
|
|
@ -72,7 +72,8 @@
|
|||
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
|
||||
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
|
||||
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
|
||||
"test:match": "mocha --timeout 20000 -g"
|
||||
"test:match": "mocha --timeout 20000 -g",
|
||||
"test:e2e": "playwright test e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^0.14.2",
|
||||
|
@ -134,7 +135,8 @@
|
|||
"zod": "^3.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.17.10",
|
||||
"@babel/types": "^7.17.0",
|
||||
"@playwright/test": "^1.21.1",
|
||||
"@types/babel__core": "^7.1.19",
|
||||
"@types/babel__generator": "^7.6.4",
|
||||
"@types/babel__traverse": "^7.17.1",
|
||||
|
|
Loading…
Reference in a new issue