1
0
mirror of synced 2025-12-19 09:57:57 -05:00

Add new integration test harness (#2189)

(meta)
This commit is contained in:
Brandon Bayer
2021-04-08 16:31:17 -04:00
committed by GitHub
parent 3c3f10b201
commit bc0c7287ec
46 changed files with 1117 additions and 133 deletions

View File

@@ -87,9 +87,10 @@ module.exports = {
},
},
{
files: ["**/__fixtures__/**"],
files: ["test/**", "**/__fixtures__/**"],
rules: {
"import/no-default-export": "off",
"require-await": "off",
},
},
],

View File

@@ -89,7 +89,7 @@ jobs:
path: ./*
key: ${{ github.sha }}
build_and_test_pkgs:
testBlitzPackages:
name: Blitz Packages Tests
needs: build
runs-on: ubuntu-latest
@@ -108,7 +108,7 @@ jobs:
env:
CI: true
build_and_test_examples:
testBlitzExamples:
timeout-minutes: 30
name: Blitz Example Apps Tests
strategy:
@@ -193,6 +193,32 @@ jobs:
- run: node run-tests.js --timings --type unit -g 1/1
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
testIntegrationBlitz:
name: Blitz - Test Integration
runs-on: ubuntu-latest
needs: build
env:
NEXT_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
strategy:
fail-fast: false
steps:
- run: echo ${{needs.build.outputs.docsChange}}
- uses: actions/cache@v2
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
id: restore-build
with:
path: ./*
key: ${{ github.sha }}
# TODO: remove after we fix watchpack watching too much
- run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
- run: xvfb-run node nextjs/run-tests.js -c 3
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
testIntegration:
name: Nextjs - Test Integration
defaults:
@@ -259,7 +285,15 @@ jobs:
testsPass:
name: thank you, next
runs-on: ubuntu-latest
needs: [checkPrecompiled, testIntegration, testUnit]
needs:
[
checkPrecompiled,
testIntegration,
testIntegrationBlitz,
testUnit,
testBlitzPackages,
testBlitzExamples,
]
steps:
- run: exit 0

View File

@@ -4,7 +4,7 @@
.log
.DS_Store
.jest-*
lib
packages/cli/lib
node_modules
reports
*.log

View File

@@ -26,4 +26,24 @@ module.exports = {
},
],
],
overrides: [
{
test: "./test/**/*",
presets: [
[
"@babel/preset-env",
{
modules: false,
// loose: true,
exclude: [
"@babel/plugin-transform-async-to-generator",
"@babel/plugin-transform-regenerator",
],
},
],
"blitz/babel",
],
plugins: [],
},
],
}

View File

@@ -1,27 +1,25 @@
const withMonorepoBuildTooling = require("@preconstruct/next")
const {sessionMiddleware, simpleRolesIsAuthorized} = require("blitz")
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
})
module.exports = withMonorepoBuildTooling(
withBundleAnalyzer({
middleware: [
sessionMiddleware({
isAuthorized: simpleRolesIsAuthorized,
// sessionExpiryMinutes: 4,
}),
],
cli: {
clearConsoleOnBlitzDev: false,
},
log: {
// level: "trace",
},
experimental: {
isomorphicResolverImports: false,
},
/*
module.exports = withBundleAnalyzer({
middleware: [
sessionMiddleware({
isAuthorized: simpleRolesIsAuthorized,
// sessionExpiryMinutes: 4,
}),
],
cli: {
clearConsoleOnBlitzDev: false,
},
log: {
// level: "trace",
},
experimental: {
isomorphicResolverImports: false,
},
/*
webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => {
// Note: we provide webpack above so you should not `require` it
// Perform customizations to webpack config
@@ -34,5 +32,4 @@ module.exports = withMonorepoBuildTooling(
return config
},
*/
}),
)
})

View File

@@ -1,7 +1,6 @@
const {sessionMiddleware, simpleRolesIsAuthorized} = require("blitz")
const withMonorepoBuildTooling = require("@preconstruct/next")
module.exports = withMonorepoBuildTooling({
module.exports = {
middleware: [
sessionMiddleware({
isAuthorized: simpleRolesIsAuthorized,
@@ -15,4 +14,4 @@ module.exports = withMonorepoBuildTooling({
return config
},
*/
})
}

View File

@@ -1,5 +1,4 @@
const { sessionMiddleware, simpleRolesIsAuthorized } = require("blitz")
const withMonorepoBuildTooling = require("@preconstruct/next")
const { GraphQLClient, gql } = require("graphql-request")
const graphQLClient = new GraphQLClient("https://graphql.fauna.com/graphql", {
@@ -18,7 +17,7 @@ const normalizeSession = (faunaSession) => {
}
}
module.exports = withMonorepoBuildTooling({
module.exports = {
middleware: [
sessionMiddleware({
isAuthorized: simpleRolesIsAuthorized,
@@ -159,4 +158,4 @@ module.exports = withMonorepoBuildTooling({
return config
},
*/
})
}

View File

@@ -1,6 +1,4 @@
const withMonorepoBuildTooling = require("@preconstruct/next")
module.exports = withMonorepoBuildTooling({
module.exports = {
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// Note: we provide webpack above so you should not `require` it
// Perform customizations to webpack config
@@ -12,4 +10,4 @@ module.exports = withMonorepoBuildTooling({
// Important: return the modified config
return config
},
})
}

View File

@@ -1,6 +1,4 @@
const withMonorepoBuildTooling = require("@preconstruct/next")
module.exports = withMonorepoBuildTooling({
module.exports = {
middleware: [],
/* Uncomment this to customize the webpack config
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
@@ -10,4 +8,4 @@ module.exports = withMonorepoBuildTooling({
return config
},
*/
})
}

View File

@@ -1,6 +1,4 @@
const withMonorepoBuildTooling = require("@preconstruct/next")
module.exports = withMonorepoBuildTooling({
module.exports = {
middleware: [
(req, res, next) => {
res.blitzCtx.referer = req.headers.referer
@@ -24,4 +22,4 @@ module.exports = withMonorepoBuildTooling({
// // Important: return the modified config
// return config
// },
})
}

37
jest-unit.config.js Normal file
View File

@@ -0,0 +1,37 @@
const {jsWithBabel: tsjPreset} = require("ts-jest/preset")
module.exports = {
testEnvironment: "node",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
modulePathIgnorePatterns: ["<rootDir>/tmp", "<rootDir>/dist", "<rootDir>/templates"],
moduleNameMapper: {},
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
transform: {
...tsjPreset.transform,
},
transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"],
testMatch: ["<rootDir>/**/*.(spec|test).{ts,tsx,js,jsx}"],
testURL: "http://localhost",
// watchPlugins: [
// require.resolve("jest-watch-typeahead/filename"),
// require.resolve("jest-watch-typeahead/testname"),
// ],
coverageReporters: ["json", "lcov", "text", "clover"],
// collectCoverage: !!`Boolean(process.env.CI)`,
collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"],
coveragePathIgnorePatterns: ["/templates/"],
// coverageThreshold: {
// global: {
// branches: 100,
// functions: 100,
// lines: 100,
// statements: 100,
// },
// },
globals: {
"ts-jest": {
tsconfig: __dirname + "/tsconfig.test.json",
isolatedModules: true,
},
},
}

View File

@@ -1,37 +1,10 @@
const {jsWithBabel: tsjPreset} = require("ts-jest/preset")
module.exports = {
testEnvironment: "node",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
modulePathIgnorePatterns: ["<rootDir>/tmp", "<rootDir>/dist", "<rootDir>/templates"],
moduleNameMapper: {},
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
transform: {
...tsjPreset.transform,
},
transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"],
testMatch: ["<rootDir>/**/*.(spec|test).{ts,tsx,js,jsx}"],
testURL: "http://localhost",
// watchPlugins: [
// require.resolve("jest-watch-typeahead/filename"),
// require.resolve("jest-watch-typeahead/testname"),
// ],
coverageReporters: ["json", "lcov", "text", "clover"],
// collectCoverage: !!`Boolean(process.env.CI)`,
collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"],
coveragePathIgnorePatterns: ["/templates/"],
// coverageThreshold: {
// global: {
// branches: 100,
// functions: 100,
// lines: 100,
// statements: 100,
// },
// },
globals: {
"ts-jest": {
tsconfig: __dirname + "/tsconfig.test.json",
isolatedModules: true,
},
},
testMatch: ["**/*.test.js", "**/*.test.ts"],
verbose: true,
rootDir: "test",
modulePaths: ["<rootDir>/lib"],
globalSetup: "<rootDir>/jest-global-setup.js",
globalTeardown: "<rootDir>/jest-global-teardown.js",
setupFilesAfterEnv: ["<rootDir>/jest-setup-after-env.js"],
testEnvironment: "<rootDir>/jest-environment.js",
}

View File

@@ -48,7 +48,7 @@
"dev": "taskr",
"release": "taskr release",
"prepublish": "npm run release && yarn types",
"types": "tsc --declaration --emitDeclarationOnly --declarationDir dist",
"types": "rimraf \"dist/**/*.d.ts\" && tsc --declaration --emitDeclarationOnly --declarationDir dist",
"typescript": "tsc --noEmit --declaration",
"ncc-compiled": "ncc cache clean && taskr ncc"
},

View File

@@ -64,7 +64,7 @@ async function main() {
tests = (
await glob('**/*.test.js', {
nodir: true,
cwd: path.join(__dirname, 'test'),
cwd: path.join(process.cwd(), 'test'),
})
).filter((test) => {
// only include the specified type
@@ -79,7 +79,7 @@ async function main() {
if (outputTimings && groupArg) {
console.log('Fetching previous timings data')
try {
const timingsFile = path.join(__dirname, 'test-timings.json')
const timingsFile = path.join(process.cwd(), 'test-timings.json')
try {
prevTimings = JSON.parse(await fs.readFile(timingsFile, 'utf8'))
console.log('Loaded test timings from disk successfully')
@@ -234,7 +234,7 @@ async function main() {
} catch (err) {
if (i < NUM_RETRIES) {
try {
const testDir = path.dirname(path.join(__dirname, test))
const testDir = path.dirname(path.join(process.cwd(), test))
console.log('Cleaning test files at', testDir)
await exec(`git clean -fdx "${testDir}"`)
await exec(`git checkout "${testDir}"`)

View File

@@ -24,17 +24,23 @@
},
"scripts": {
"postinstall": "husky install && patch-package && symlink-dir node_modules/@blitzjs/next node_modules/next",
"wait:nextjs": "wait-on -d 1000 nextjs/packages/next/dist/build/index.js",
"wait:nextjs-types": "wait-on -d 1000 nextjs/packages/next/dist/build/index.d.ts",
"dev:nextjs": "yarn workspace @blitzjs/next dev",
"dev:tsc": "tsc --watch --pretty --preserveWatchOutput",
"dev:cli": "yarn workspace @blitzjs/cli dev",
"dev:nextjs-types": "yarn wait:nextjs && yarn workspace @blitzjs/next types && echo 'Finished building nextjs types'",
"dev:blitz": "preconstruct watch",
"dev:tsc": "yarn dev:nextjs-types && tsc --watch --pretty --preserveWatchOutput",
"dev:cli": "yarn wait:nextjs && yarn workspace @blitzjs/cli dev",
"dev:templates": "yarn workspace @blitzjs/generator dev",
"dev": "yarn workspace @blitzjs/next prepublish && preconstruct dev && concurrently --names \"typecheck,cli,templates\" -c \"blue,green,yellow,magenta\" -p \"{name}\" \"npm:dev:tsc\" \"npm:dev:cli\" \"npm:dev:templates\"",
"dev": "concurrently --names \"nextjs,blitz,typecheck,cli,templates\" -c \"magenta,cyan,green,yellow,black\" -p \"{name}\" \"npm:dev:nextjs\" \"npm:dev:blitz\" \"npm:dev:tsc\" \"npm:dev:cli\" \"npm:dev:templates\"",
"build:nextjs": "yarn workspace @blitzjs/next prepublish",
"build": "yarn build:nextjs && cross-env BLITZ_PROD_BUILD=true preconstruct build && ultra -r --filter \"packages/*\" buildpkg && tsc",
"lint": "eslint --ext \".js,.ts,.tsx\" .",
"link-cli": "yarn workspace blitz link",
"unlink-cli": "yarn workspace blitz unlink",
"test": "yarn run lint && yarn run build && ultra -r test",
"testheadless": "cross-env HEADLESS=true yarn test:integration",
"test:integration": "jest --runInBand",
"test:packages": "yarn run build && yarn testonly:packages",
"test:examples": "yarn run build && yarn testonly:examples",
"test:nextjs-size": "yarn --cwd nextjs testheadless --testPathPattern \"integration/(build-output|size-limit|fallback-modules)\"",
@@ -77,8 +83,7 @@
"@babel/preset-typescript": "7.12.7",
"@juanm04/cpx": "2.0.0",
"@manypkg/cli": "0.17.0",
"@preconstruct/cli": "2.0.5",
"@preconstruct/next": "2.0.0",
"@preconstruct/cli": "2.0.7",
"@rollup/pluginutils": "4.1.0",
"@size-limit/preset-small-lib": "4.9.2",
"@testing-library/jest-dom": "5.11.9",
@@ -95,6 +100,7 @@
"@types/flush-write-stream": "1.0.0",
"@types/from2": "2.3.0",
"@types/fs-extra": "8.1.0",
"@types/get-port": "4.2.0",
"@types/gulp-if": "0.0.33",
"@types/htmlescape": "^1.1.1",
"@types/ink-spinner": "3.0.0",
@@ -153,6 +159,7 @@
"eslint-plugin-simple-import-sort": "7.0.0",
"eslint-plugin-unicorn": "26.0.1",
"eslint_d": "10.0.0",
"get-port": "5.1.1",
"husky": "5.1.2",
"jest": "27.0.0-next.5",
"lerna": "4.0.0",
@@ -179,6 +186,7 @@
"stdout-stderr": "0.1.13",
"strip-ansi": "6.0.0",
"test-listen": "1.1.0",
"tree-kill": "1.2.2",
"ts-jest": "27.0.0-next.10",
"tslib": "2.1.0",
"typescript": "4.1.5",

View File

@@ -1,4 +1,4 @@
module.exports = {
preset: '../../jest.config.js',
preset: '../../jest-unit.config.js',
testEnvironment: 'jest-environment-jsdom',
};

View File

@@ -4,7 +4,8 @@ import RewriteImports from './rewrite-imports';
// eslint-disable-next-line import/no-default-export
export default function preset(_api: any, options = {}) {
// const isTest = _api.env('test');
const isRunningInJest = Boolean(process.env.JEST_WORKER_ID);
const isRunningInJest =
process.env.JEST_WORKER_ID && !process.env.__NEXT_TEST_MODE;
const config = {
presets: [[require('next/babel'), options]],

View File

@@ -1,3 +1,3 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
}

View File

@@ -1,5 +1,5 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
// collectCoverage: !!`Boolean(process.env.CI)`,
modulePathIgnorePatterns: ["<rootDir>/tmp", "<rootDir>/lib", "<rootDir>/commands/.test"],
testPathIgnorePatterns: ["src/commands/test.ts", "test/commands/.test"],

View File

@@ -40,7 +40,10 @@ export class Dev extends Command {
const {getConfig} = await import("@blitzjs/config")
const blitzConfig = getConfig()
if (blitzConfig.cli?.clearConsoleOnBlitzDev !== false) {
if (
blitzConfig.cli?.clearConsoleOnBlitzDev !== false &&
!process.env.BLITZ_TEST_ENVIRONMENT
) {
const {log} = await import("@blitzjs/display")
log.clearConsole()
}

View File

@@ -1,4 +1,4 @@
import {readJSON} from "fs-extra"
import {existsSync, readJSON} from "fs-extra"
import {resolve} from "path"
import pkgDir from "pkg-dir"
@@ -54,6 +54,10 @@ export const isBlitzRoot = async (): Promise<{
if (err.code === "ENOENT") {
const out = await checkParent()
if (existsSync("./blitz.config.js") || existsSync("./blitz.config.ts")) {
return {err: false}
}
if (out === false) {
return {
err: true,

View File

@@ -1,3 +1,3 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
}

View File

@@ -5,7 +5,9 @@ import pkgDir from "pkg-dir"
const debug = require("debug")("blitz:config")
export function getProjectRoot() {
return pkgDir.sync() || process.cwd()
return (
path.dirname(path.resolve(process.cwd(), "blitz.config.js")) || pkgDir.sync() || process.cwd()
)
}
export interface BlitzConfig extends Record<string, unknown> {
@@ -40,11 +42,16 @@ export const getConfig = (reload?: boolean): BlitzConfig => {
const {PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_SERVER} = require("next/constants")
const pkgJson = readJSONSync(join(getProjectRoot(), "package.json"))
let pkgJson: any
const pkgJsonPath = join(getProjectRoot(), "package.json")
if (existsSync(pkgJsonPath)) {
pkgJson = readJSONSync(join(getProjectRoot(), "package.json"))
}
let blitzConfig = {
_meta: {
packageName: pkgJson.name,
packageName: pkgJson?.name,
},
}
@@ -89,10 +96,10 @@ export const getConfig = (reload?: boolean): BlitzConfig => {
...loadedNextConfig,
...loadedBlitzConfig,
}
} catch {
} catch (error) {
// https://github.com/blitz-js/blitz/issues/2080
if (!process.env.JEST_WORKER_ID) {
console.error("Failed to load config in getConfig()")
console.error("Failed to load config in getConfig()", error)
}
}

View File

@@ -1,4 +1,4 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
testEnvironment: "jest-environment-jsdom",
}

View File

@@ -1,3 +1,3 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
}

View File

@@ -1,3 +1,3 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
}

View File

@@ -1,3 +1,3 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
}

View File

@@ -1,3 +1,3 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
}

View File

@@ -1,3 +1,3 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
}

View File

@@ -1,3 +1,3 @@
module.exports = {
preset: "../../jest.config.js",
preset: "../../jest-unit.config.js",
}

View File

@@ -1,13 +0,0 @@
diff --git a/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js b/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js
index 7e40755..fde60cc 100644
--- a/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js
+++ b/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js
@@ -1770,6 +1770,8 @@ let getRollupConfig = (pkg, entrypoints, aliases, type, reportTransformedFile) =
external.push(...builtInModules);
}
+ external.push('next', 'react', '@babel/core', 'prettier')
+
let input = {};
entrypoints.forEach(entrypoint => {
input[path__default.relative(pkg.directory, path__default.join(entrypoint.directory, "dist", getNameForDistForEntrypoint(entrypoint)))] = entrypoint.source;

View File

@@ -0,0 +1,22 @@
diff --git a/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js b/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js
index 972d57b..e6bc64b 100644
--- a/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js
+++ b/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js
@@ -166,7 +166,7 @@ function format(args, messageType, scope) {
info: chalk.cyan("info"),
none: ""
}[messageType];
- let fullPrefix = "🎁 " + prefix + (scope === undefined ? "" : " " + chalk.cyan(scope));
+ let fullPrefix = prefix + (scope === undefined ? "" : " " + chalk.cyan(scope));
return fullPrefix + util.format("", ...args).split("\n").reduce((str, line) => {
const prefixed = `${str}\n${fullPrefix}`;
return line ? `${prefixed} ${line}` : prefixed;
@@ -1917,6 +1917,8 @@ let getRollupConfig = (pkg, entrypoints, aliases, type, reportTransformedFile) =
external.push(...builtInModules);
}
+ external.push('next', 'react', '@babel/core', 'prettier', '.blitz')
+
let input = {};
entrypoints.forEach(entrypoint => {
input[path__default.relative(pkg.directory, path__default.join(entrypoint.directory, "dist", getNameForDistForEntrypoint(entrypoint)))] = entrypoint.source;

View File

@@ -0,0 +1,4 @@
module.exports = {
presets: ["blitz/babel"],
plugins: [],
}

View File

@@ -0,0 +1,11 @@
module.exports = {
// replace me
async rewrites() {
return [
{
source: "/blog/post/:pid",
destination: "/blog/:pid",
},
]
},
}

View File

@@ -0,0 +1,16 @@
import {useRouter} from "blitz"
import React from "react"
const Post = () => {
const router = useRouter()
return (
<>
<div id="as-path">{router.asPath}</div>
</>
)
}
Post.getInitialProps = () => ({hello: "hi"})
export default Post

View File

@@ -0,0 +1,3 @@
const page = () => "hello from sub id"
page.getInitialProps = () => ({hello: "hi"})
export default page

View File

@@ -0,0 +1 @@
export default () => <p id="normal-text">a normal page</p>

View File

@@ -0,0 +1,81 @@
import {
blitzBuild,
blitzStart,
File,
findPort,
killApp,
launchApp,
renderViaHTTP,
} from "blitz-test-utils"
import cheerio from "cheerio"
import {join} from "path"
jest.setTimeout(1000 * 60 * 5)
let app: any
let appPort: number
const appDir = join(__dirname, "..")
const blitzConfig = new File(join(appDir, "blitz.config.js"))
const runTests = () => {
it("should have gip in __NEXT_DATA__", async () => {
const html = await renderViaHTTP(appPort, "/")
const $ = cheerio.load(html)
expect(JSON.parse($("#__NEXT_DATA__").text()).gip).toBe(true)
})
it("should not have gip in __NEXT_DATA__ for non-GIP page", async () => {
const html = await renderViaHTTP(appPort, "/normal")
const $ = cheerio.load(html)
expect("gip" in JSON.parse($("#__NEXT_DATA__").text())).toBe(false)
})
it("should have correct router.asPath for direct visit dynamic page", async () => {
const html = await renderViaHTTP(appPort, "/blog/1")
const $ = cheerio.load(html)
expect($("#as-path").text()).toBe("/blog/1")
})
it("should have correct router.asPath for direct visit dynamic page rewrite direct", async () => {
const html = await renderViaHTTP(appPort, "/blog/post/1")
const $ = cheerio.load(html)
expect($("#as-path").text()).toBe("/blog/post/1")
})
}
describe("getInitialProps", () => {
describe("dev mode", () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(() => killApp(app))
runTests()
})
describe("serverless mode", () => {
beforeAll(async () => {
blitzConfig.replace("// replace me", `target: 'serverless', `)
await blitzBuild(appDir)
appPort = await findPort()
app = await blitzStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
blitzConfig.restore()
})
runTests()
})
describe("production mode", () => {
beforeAll(async () => {
await blitzBuild(appDir)
appPort = await findPort()
app = await blitzStart(appDir, appPort)
})
afterAll(() => killApp(app))
runTests()
})
})

89
test/jest-environment.js Normal file
View File

@@ -0,0 +1,89 @@
// my-custom-environment
const http = require("http")
const getPort = require("get-port")
const seleniumServer = require("selenium-standalone")
const NodeEnvironment = require("jest-environment-node")
const {BROWSER_NAME: browserName = "chrome", SKIP_LOCAL_SELENIUM_SERVER} = process.env
const newTabPg = `
<!DOCTYPE html>
<html>
<head>
<title>new tab</title>
</head>
<body>
<a href="about:blank" target="_blank" id="new">Click me</a>
</body>
</html>
`
class CustomEnvironment extends NodeEnvironment {
async setup() {
await super.setup()
// Since ie11 doesn't like dataURIs we have to spin up a
// server to handle the new tab page
this.server = http.createServer((req, res) => {
res.statusCode = 200
res.end(newTabPg)
})
const newTabPort = await getPort()
await new Promise((resolve, reject) => {
this.server.listen(newTabPort, (err) => {
if (err) return reject(err)
resolve()
})
})
let seleniumServerPort
if (browserName !== "chrome" && SKIP_LOCAL_SELENIUM_SERVER !== "true") {
console.log("Installing selenium server")
await new Promise((resolve, reject) => {
seleniumServer.install((err) => {
if (err) return reject(err)
resolve()
})
})
console.log("Starting selenium server")
await new Promise((resolve, reject) => {
seleniumServer.start((err, child) => {
if (err) return reject(err)
this.seleniumServer = child
resolve()
})
})
console.log("Started selenium server")
seleniumServerPort = 4444
}
this.global.wd = null
this.global._newTabPort = newTabPort
this.global.browserName = browserName
this.global.seleniumServerPort = seleniumServerPort
this.global.browserStackLocalId = global.browserStackLocalId
}
async teardown() {
await super.teardown()
if (this.server) {
this.server.close()
}
if (this.global.wd) {
try {
await this.global.wd.quit()
} catch (err) {
console.log(`Failed to quit webdriver instance`, err)
}
}
// must come after wd.quit()
if (this.seleniumServer) {
this.seleniumServer.kill()
}
}
}
module.exports = CustomEnvironment

24
test/jest-global-setup.js Normal file
View File

@@ -0,0 +1,24 @@
let globalSetup = () => {}
if (process.env.BROWSERSTACK) {
const {Local} = require("browserstack-local")
const browserStackLocal = new Local()
const localBrowserStackOpts = {
key: process.env.BROWSERSTACK_ACCESS_KEY,
localIdentifier: new Date().getTime(), // Adding a unique local identifier to run parallel tests on BrowserStack
}
global.browserStackLocal = browserStackLocal
global.browserStackLocalId = localBrowserStackOpts.localIdentifier
globalSetup = () => {
return new Promise((resolve, reject) => {
browserStackLocal.start(localBrowserStackOpts, (err) => {
if (err) return reject(err)
console.log("Started BrowserStackLocal", browserStackLocal.isRunning())
resolve()
})
})
}
}
module.exports = globalSetup

View File

@@ -0,0 +1,9 @@
let globalTeardown = () => {}
if (process.env.BROWSERSTACK) {
globalTeardown = () => global.browserStackLocal.killAllProcesses(() => {})
}
module.exports = async () => {
await globalTeardown()
}

View File

@@ -0,0 +1,9 @@
/* eslint-env jest */
process.env.BLITZ_TEST_ENVIRONMENT = true
if (process.env.JEST_RETRY_TIMES) {
const retries = Number(process.env.JEST_RETRY_TIMES)
console.log(`Configuring jest retries: ${retries}`)
jest.retryTimes(retries)
}

View File

@@ -0,0 +1,640 @@
import {ChildProcess} from "child_process"
import spawn from "cross-spawn"
import express from "express"
import {existsSync, readFileSync, unlinkSync, writeFileSync} from "fs"
import {writeFile} from "fs-extra"
import getPort from "get-port"
import http from "http"
// `next` here is the symlink in `test/node_modules/next` which points to the root directory.
// This is done so that requiring from `next` works.
// The reason we don't import the relative path `../../dist/<etc>` is that it would lead to inconsistent module singletons
// import server from "next/dist/server/next"
import _pkg from "next/package.json"
import fetch from "node-fetch"
import path from "path"
import qs from "querystring"
import treeKill from "tree-kill"
// export const nextServer = server
export const pkg = _pkg
// polyfill Object.fromEntries for the test/integration/relay-analytics tests
// on node 10, this can be removed after we no longer support node 10
if (!Object.fromEntries) {
Object.fromEntries = require("core-js/features/object/from-entries")
}
export function initBlitzServerScript(
scriptPath: string,
successRegexp: RegExp,
env: Record<any, any>,
failRegexp: RegExp,
opts?: {
onStdout?: (stdout: string) => void
onStderr?: (stderr: string) => void
},
) {
return new Promise((resolve, reject) => {
const instance = spawn("node", ["--no-deprecation", scriptPath], {env})
function handleStdout(data: Buffer) {
const message = data.toString()
if (successRegexp.test(message)) {
resolve(instance)
}
process.stdout.write(message)
if (opts && opts.onStdout) {
opts.onStdout(message.toString())
}
}
function handleStderr(data: Buffer) {
const message = data.toString()
if (failRegexp && failRegexp.test(message)) {
instance.kill()
return reject(new Error("received failRegexp"))
}
process.stderr.write(message)
if (opts && opts.onStderr) {
opts.onStderr(message.toString())
}
}
instance.stdout.on("data", handleStdout)
instance.stderr.on("data", handleStderr)
instance.on("close", () => {
instance.stdout.removeListener("data", handleStdout)
instance.stderr.removeListener("data", handleStderr)
})
instance.on("error", (err) => {
reject(err)
})
})
}
export function renderViaAPI(app: any, pathname: string, query: Record<any, any>) {
const url = `${pathname}${query ? `?${qs.stringify(query)}` : ""}`
return app.renderToHTML({url}, {}, pathname, query)
}
export async function renderViaHTTP(
appPort: number,
pathname: string,
query?: Record<any, any>,
opts?: Record<any, any>,
) {
return fetchViaHTTP(appPort, pathname, query, opts).then((res) => res.text())
}
export function fetchViaHTTP(
appPort: number,
pathname: string,
query?: Record<any, any>,
opts?: Record<any, any>,
) {
const url = `http://localhost:${appPort}${pathname}${query ? `?${qs.stringify(query)}` : ""}`
return fetch(url, opts)
}
export function findPort() {
return getPort()
}
interface RunBlitzCommandOptions {
cwd?: string
env?: Record<any, any>
spawnOptions?: any
instance?: any
stderr?: boolean
stdout?: boolean
ignoreFail?: boolean
}
export function runBlitzCommand(argv: any[], options: RunBlitzCommandOptions = {}) {
const blitzDir = path.dirname(require.resolve("blitz/package"))
const blitzBin = path.join(blitzDir, "bin/blitz")
const cwd = options.cwd || blitzDir
// Let Next.js decide the environment
const env = {
...process.env,
...options.env,
NODE_ENV: "",
__NEXT_TEST_MODE: "true",
}
return new Promise((resolve, reject) => {
console.log(`Running command "blitz ${argv.join(" ")}"`)
const instance = spawn("node", ["--no-deprecation", blitzBin, ...argv], {
...options.spawnOptions,
cwd,
env,
stdio: ["ignore", "pipe", "pipe"],
})
if (typeof options.instance === "function") {
options.instance(instance)
}
let stderrOutput = ""
if (options.stderr) {
instance.stderr.on("data", function (chunk) {
stderrOutput += chunk
})
}
let stdoutOutput = ""
if (options.stdout) {
instance.stdout.on("data", function (chunk) {
stdoutOutput += chunk
})
}
instance.on("close", (code, signal) => {
if (!options.stderr && !options.stdout && !options.ignoreFail && code !== 0) {
console.log(stderrOutput)
return reject(new Error(`command failed with code ${code}`))
}
resolve({
code,
signal,
stdout: stdoutOutput,
stderr: stderrOutput,
})
})
instance.on("error", (err: any) => {
console.log(stderrOutput)
err.stdout = stdoutOutput
err.stderr = stderrOutput
reject(err)
})
})
}
interface RunBlitzLaunchOptions {
cwd?: string
env?: Record<any, any>
onStdout?: (stdout: string) => void
onStderr?: (stderr: string) => void
stderr?: boolean
stdout?: boolean
blitzStart?: boolean
}
export function runBlitzLaunchCommand(
argv: any[],
stdOut: unknown,
opts: RunBlitzLaunchOptions = {},
) {
const blitzDir = path.dirname(require.resolve("blitz/package"))
const blitzBin = path.join(blitzDir, "bin/blitz")
const cwd = opts.cwd ?? path.dirname(require.resolve("blitz/package"))
console.log(cwd)
const env = {
...process.env,
NODE_ENV: undefined,
__NEXT_TEST_MODE: "true",
...opts.env,
}
return new Promise<void | string | ChildProcess>((resolve, reject) => {
const instance = spawn(
"node",
["--no-deprecation", blitzBin, opts.blitzStart ? "start" : "dev", ...argv],
{cwd, env},
)
let didResolve = false
function handleStdout(data: Buffer) {
const message = data.toString()
const bootupMarkers = {
dev: /compiled successfully/i,
start: /started server/i,
}
if (bootupMarkers[opts.blitzStart || stdOut ? "start" : "dev"].test(message)) {
if (!didResolve) {
didResolve = true
resolve(stdOut ? message : instance)
}
}
if (typeof opts.onStdout === "function") {
opts.onStdout(message)
}
if (opts.stdout !== false) {
process.stdout.write(message)
}
}
function handleStderr(data: Buffer) {
const message = data.toString()
if (typeof opts.onStderr === "function") {
opts.onStderr(message)
}
if (opts.stderr !== false) {
process.stderr.write(message)
}
}
instance.stdout.on("data", handleStdout)
instance.stderr.on("data", handleStderr)
instance.on("close", () => {
instance.stdout.removeListener("data", handleStdout)
instance.stderr.removeListener("data", handleStderr)
if (!didResolve) {
didResolve = true
resolve()
}
})
instance.on("error", (err) => {
reject(err)
})
})
}
// Launch the app in dev mode.
export function launchApp(dir: string, port: number, opts: RunBlitzLaunchOptions = {}) {
return runBlitzLaunchCommand(["-p", port], undefined, {cwd: dir, ...opts})
}
export function blitzBuild(dir: string, args = [], opts: RunBlitzCommandOptions = {}) {
return runBlitzCommand(["build", ...args], {cwd: dir, ...opts})
}
export function blitzExport(dir: string, {outdir}, opts: RunBlitzCommandOptions = {}) {
return runBlitzCommand(["export", "--outdir", outdir], {cwd: dir, ...opts})
}
export function blitzExportDefault(dir: string, opts: RunBlitzCommandOptions = {}) {
return runBlitzCommand(["export"], {cwd: dir, ...opts})
}
export function blitzStart(dir: string, port: number, opts: RunBlitzLaunchOptions = {}) {
return runBlitzLaunchCommand(["-p", port], undefined, {
cwd: dir,
...opts,
blitzStart: true,
})
}
export function buildTS(args = [], cwd: string, env = {}) {
cwd = cwd || path.dirname(require.resolve("@blitzjs/cli/package"))
env = {...process.env, NODE_ENV: undefined, ...env}
return new Promise<void>((resolve, reject) => {
const instance = spawn(
"node",
["--no-deprecation", require.resolve("typescript/lib/tsc"), ...args],
{cwd, env},
)
let output = ""
const handleData = (chunk) => {
output += chunk.toString()
}
instance.stdout.on("data", handleData)
instance.stderr.on("data", handleData)
instance.on("exit", (code) => {
if (code) {
return reject(new Error("exited with code: " + code + "\n" + output))
}
resolve()
})
})
}
// Kill a launched app
export async function killApp(instance) {
await new Promise<void>((resolve, reject) => {
treeKill(instance.pid, (err) => {
if (err) {
if (
process.platform === "win32" &&
typeof err.message === "string" &&
(err.message.includes(`no running instance of the task`) ||
err.message.includes(`not found`))
) {
// Windows throws an error if the process is already dead
//
// Command failed: taskkill /pid 6924 /T /F
// ERROR: The process with PID 6924 (child process of PID 6736) could not be terminated.
// Reason: There is no running instance of the task.
return resolve()
}
return reject(err)
}
resolve()
})
})
}
export async function startApp(app: any) {
await app.prepare()
const handler = app.getRequestHandler()
const server = http.createServer(handler)
;(server as any).__app = app
await promiseCall(server, "listen")
return server
}
export async function stopApp(server: any) {
if (server.__app) {
await server.__app.close()
}
await promiseCall(server, "close")
}
export function promiseCall(obj: any, method: any, ...args: any[]) {
return new Promise((resolve, reject) => {
const newArgs = [
...args,
function (err: any, res: any) {
if (err) return reject(err)
resolve(res)
},
]
obj[method](...newArgs)
})
}
export function waitFor(millis: number) {
return new Promise((resolve) => setTimeout(resolve, millis))
}
export async function startStaticServer(dir: string) {
const app = express()
const server = http.createServer(app)
app.use(express.static(dir))
await promiseCall(server, "listen")
return server
}
export async function startCleanStaticServer(dir: string) {
const app = express()
const server = http.createServer(app)
app.use(express.static(dir, {extensions: ["html"]}))
await promiseCall(server, "listen")
return server
}
// check for content in 1 second intervals timing out after
// 30 seconds
export async function check(contentFn: Function, regex: RegExp, hardError = true) {
let content: any
let lastErr: any
for (let tries = 0; tries < 30; tries++) {
try {
content = await contentFn()
if (typeof regex === "string") {
if (regex === content) {
return true
}
} else if (regex.test(content)) {
// found the content
return true
}
await waitFor(1000)
} catch (err) {
await waitFor(1000)
lastErr = err
}
}
console.error("TIMED OUT CHECK: ", {regex, content, lastErr})
if (hardError) {
throw new Error("TIMED OUT: " + regex + "\n\n" + content)
}
return false
}
export class File {
path: string
originalContent: any
constructor(path: string) {
this.path = path
this.originalContent = existsSync(this.path) ? readFileSync(this.path, "utf8") : null
}
write(content: any) {
if (!this.originalContent) {
this.originalContent = content
}
writeFileSync(this.path, content, "utf8")
}
replace(pattern: any, newValue: any) {
const currentContent = readFileSync(this.path, "utf8")
if (pattern instanceof RegExp) {
if (!pattern.test(currentContent)) {
throw new Error(
`Failed to replace content.\n\nPattern: ${pattern.toString()}\n\nContent: ${currentContent}`,
)
}
} else if (typeof pattern === "string") {
if (!currentContent.includes(pattern)) {
throw new Error(
`Failed to replace content.\n\nPattern: ${pattern}\n\nContent: ${currentContent}`,
)
}
} else {
throw new Error(`Unknown replacement attempt type: ${pattern}`)
}
const newContent = currentContent.replace(pattern, newValue)
this.write(newContent)
}
delete() {
unlinkSync(this.path)
}
restore() {
this.write(this.originalContent)
}
}
export async function evaluate(browser: any, input: any) {
if (typeof input === "function") {
const result = await browser.executeScript(input)
await new Promise((resolve) => setTimeout(resolve, 30))
return result
} else {
throw new Error(`You must pass a function to be evaluated in the browser.`)
}
}
export async function retry(fn: Function, duration = 3000, interval = 500, description: string) {
if (duration % interval !== 0) {
throw new Error(
`invalid duration ${duration} and interval ${interval} mix, duration must be evenly divisible by interval`,
)
}
for (let i = duration; i >= 0; i -= interval) {
try {
return await fn()
} catch (err) {
if (i === 0) {
console.error(`Failed to retry${description ? ` ${description}` : ""} within ${duration}ms`)
throw err
}
console.warn(`Retrying${description ? ` ${description}` : ""} in ${interval}ms`)
await waitFor(interval)
}
}
}
export async function hasRedbox(browser: any, expected = true) {
let attempts = 30
do {
const has = await evaluate(browser, () => {
return Boolean(
[].slice
.call(document.querySelectorAll("nextjs-portal"))
.find((p: any) =>
p.shadowRoot.querySelector(
"#nextjs__container_errors_label, #nextjs__container_build_error_label",
),
),
)
})
if (has) {
return true
}
if (--attempts < 0) {
break
}
await new Promise((resolve) => setTimeout(resolve, 1000))
} while (expected)
return false
}
export async function getRedboxHeader(browser: any) {
return retry(
() =>
evaluate(browser, () => {
const portal = [].slice
.call(document.querySelectorAll("nextjs-portal"))
.find((p: any) => p.shadowRoot.querySelector("[data-nextjs-dialog-header"))
const root = portal.shadowRoot
return root
.querySelector("[data-nextjs-dialog-header]")
.innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, "Unknown")
}),
3000,
500,
"getRedboxHeader",
)
}
export async function getRedboxSource(browser: any) {
return retry(
() =>
evaluate(browser, () => {
const portal = [].slice
.call(document.querySelectorAll("nextjs-portal"))
.find((p: any) =>
p.shadowRoot.querySelector(
"#nextjs__container_errors_label, #nextjs__container_build_error_label",
),
)
const root = portal.shadowRoot
return root
.querySelector("[data-nextjs-codeframe], [data-nextjs-terminal]")
.innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, "Unknown")
}),
3000,
500,
"getRedboxSource",
)
}
export function getBrowserBodyText(browser: any) {
return browser.eval('document.getElementsByTagName("body")[0].innerText')
}
export function normalizeRegEx(src: string) {
return new RegExp(src).source.replace(/\^\//g, "^\\/")
}
function readJson(path: string) {
return JSON.parse(readFileSync(path) as any)
}
export function getBuildManifest(dir: string) {
return readJson(path.join(dir, ".next/build-manifest.json"))
}
export function getPageFileFromBuildManifest(dir: string, page: string) {
const buildManifest = getBuildManifest(dir)
const pageFiles = buildManifest.pages[page]
if (!pageFiles) {
throw new Error(`No files for page ${page}`)
}
const pageFile = pageFiles.find(
(file: string) =>
file.endsWith(".js") && file.includes(`pages${page === "" ? "/index" : page}`),
)
if (!pageFile) {
throw new Error(`No page file for page ${page}`)
}
return pageFile
}
export function readBlitzBuildClientPageFile(appDir: string, page: string) {
const pageFile = getPageFileFromBuildManifest(appDir, page)
return readFileSync(path.join(appDir, ".next", pageFile), "utf8")
}
export function getPagesManifest(dir: string) {
const serverFile = path.join(dir, ".next/server/pages-manifest.json")
if (existsSync(serverFile)) {
return readJson(serverFile)
}
return readJson(path.join(dir, ".next/serverless/pages-manifest.json"))
}
export function updatePagesManifest(dir: string, content: any) {
const serverFile = path.join(dir, ".next/server/pages-manifest.json")
if (existsSync(serverFile)) {
return writeFile(serverFile, content)
}
return writeFile(path.join(dir, ".next/serverless/pages-manifest.json"), content)
}
export function getPageFileFromPagesManifest(dir: string, page: string) {
const pagesManifest = getPagesManifest(dir)
const pageFile = pagesManifest[page]
if (!pageFile) {
throw new Error(`No file for page ${page}`)
}
return pageFile
}
export function readBlitzBuildServerPageFile(appDir: string, page: string) {
const pageFile = getPageFileFromPagesManifest(appDir, page)
return readFileSync(path.join(appDir, ".next", "server", pageFile), "utf8")
}

11
test/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
/* "module": "esnext", */
/* "target": "esnext", */
"allowJs": true,
"baseUrl": "./lib",
"resolveJsonModule": true,
"noEmit": true
}
}

View File

@@ -1,5 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["types/**/*", "packages/**/*", "recipes/**/*"],
"include": ["types/**/*", "packages/**/*", "recipes/**/*", "test/**/*"],
"exclude": []
}

View File

@@ -3131,10 +3131,10 @@
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71"
integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==
"@preconstruct/cli@2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@preconstruct/cli/-/cli-2.0.5.tgz#bd952ecae0fbe5cba37040d7f05c41cab3ac4250"
integrity sha512-yWB0GwqZi9tTpSmsEkcVYxnieCT89L9938XR7s9ffjKf59GFMwy8O0G8wMA8Rm6jNjf3ob5zuVWvCaoHppUS8Q==
"@preconstruct/cli@2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@preconstruct/cli/-/cli-2.0.7.tgz#368b0313bc3e04da2442e0133d7bdc3a076a3a55"
integrity sha512-xXKbIZa5k39fLs3ufLo2/PgZjQK/ZBzUeK0nFt+t6xE3i++e6y/RN8GNNzGxOgwgM6+m+OL7rB54ruwB/HVWqw==
dependencies:
"@babel/code-frame" "^7.5.5"
"@babel/core" "^7.7.7"
@@ -3184,13 +3184,6 @@
pirates "^4.0.1"
source-map-support "^0.5.16"
"@preconstruct/next@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@preconstruct/next/-/next-2.0.0.tgz#050042c13dde0c671bee0681acb49c31e80ab415"
integrity sha512-jpNffjgVKSilBCi3tNs2MEqqGdQBOo5n97B9OCfMDqO9SoiH7MyCmQ+tHCYQvY5gmD6Bf3Fas79N7Rzj6vJBsQ==
dependencies:
resolve "^1.17.0"
"@prisma/client@2.19.0":
version "2.19.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.19.0.tgz#a45f17a59fd109e95b61bf4b56d4a7642169ec0e"
@@ -3879,6 +3872,13 @@
dependencies:
"@types/node" "*"
"@types/get-port@4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@types/get-port/-/get-port-4.2.0.tgz#4fc44616c737d37d3ee7926d86fa975d0afba5e4"
integrity sha512-Iv2FAb5RnIk/eFO2CTu8k+0VMmIR15pKbcqRWi+s3ydW+aKXlN2yemP92SrO++ERyJx+p6Ie1ggbLBMbU1SjiQ==
dependencies:
get-port "*"
"@types/glob-stream@*":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc"
@@ -10665,7 +10665,7 @@ get-pkg-repo@^1.0.0:
parse-github-repo-url "^1.3.0"
through2 "^2.0.0"
get-port@5.1.1, get-port@^5.1.1:
get-port@*, get-port@5.1.1, get-port@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==