fix Next.rs API (#53456)

### What?

* fixes problems in Next.rs API introduced by #52846 
* adds test infrastructure for experimental turbo testing
* adds two test cases to verify the infrastructure
* add grouping of output logs in run-tests
* simplify template loading

### Why?

### How?
This commit is contained in:
Tobias Koppers 2023-08-02 14:31:52 +02:00 committed by GitHub
parent eecd8dc146
commit 61baae126f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1850 additions and 1745 deletions

View file

@ -125,6 +125,15 @@ jobs:
afterBuild: turbo run rust-check
secrets: inherit
test-experimental-turbopack-dev:
name: test experimental turbopack dev
needs: ['build-native', 'build-next']
uses: ./.github/workflows/build_reusable.yml
with:
skipForDocsOnly: 'yes'
afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/test/turbopack-tests-manifest.js" EXPERIMENTAL_TURBOPACK=1 NEXT_E2E_TEST_TIMEOUT=240000 NEXT_TEST_MODE=dev node run-tests.js --test-pattern '^(test\/development)/.*\.test\.(js|jsx|ts|tsx)$' --timings -c ${TEST_CONCURRENCY}
secrets: inherit
test-turbopack-dev:
name: test turbopack dev
needs: ['build-native', 'build-next']
@ -134,6 +143,21 @@ jobs:
afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/packages/next-swc/crates/next-dev-tests/tests-manifest.js" TURBOPACK=1 __INTERNAL_CUSTOM_TURBOPACK_BINDINGS="$(pwd)/packages/next-swc/native/next-swc.linux-x64-gnu.node" NEXT_E2E_TEST_TIMEOUT=240000 NEXT_TEST_MODE=dev node run-tests.js --test-pattern '^(test\/development)/.*\.test\.(js|jsx|ts|tsx)$' --timings -c ${TEST_CONCURRENCY}
secrets: inherit
test-experimental-turbopack-integration:
name: test experimental turbopack integration
needs: ['build-native', 'build-next']
strategy:
fail-fast: false
matrix:
group: [1]
uses: ./.github/workflows/build_reusable.yml
with:
nodeVersion: 16
skipForDocsOnly: 'yes'
afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/test/turbopack-tests-manifest.js" EXPERIMENTAL_TURBOPACK=1 node run-tests.js --timings -g ${{ matrix.group }}/1 -c ${TEST_CONCURRENCY} --type integration
secrets: inherit
test-turbopack-integration:
name: test turbopack integration
needs: ['build-native', 'build-next']
@ -148,7 +172,6 @@ jobs:
skipForDocsOnly: 'yes'
afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/packages/next-swc/crates/next-dev-tests/tests-manifest.js" TURBOPACK=1 __INTERNAL_CUSTOM_TURBOPACK_BINDINGS="$(pwd)/packages/next-swc/native/next-swc.linux-x64-gnu.node" node run-tests.js --timings -g ${{ matrix.group }}/5 -c ${TEST_CONCURRENCY} --type integration
secrets: inherit
test-next-swc-wasm:
name: test next-swc wasm
needs: ['build-native', 'build-next']
@ -244,7 +267,9 @@ jobs:
'rust-check',
'test-next-swc-wasm',
'test-turbopack-dev',
'test-experimental-turbopack-dev',
'test-turbopack-integration',
'test-experimental-turbopack-integration',
]
if: always()

View file

@ -451,6 +451,7 @@ impl AppEndpoint {
loader_tree,
self.app_project.app_dir(),
self.pathname.clone(),
self.original_name.clone(),
self.app_project.project().project_path(),
)
}
@ -462,6 +463,7 @@ impl AppEndpoint {
self.app_project.edge_rsc_module_context(),
Vc::upcast(FileSource::new(path)),
self.pathname.clone(),
self.original_name.clone(),
self.app_project.project().project_path(),
)
}

View file

@ -641,7 +641,7 @@ impl PageEndpoint {
.project()
.node_root()
.join("server".to_string()),
this.path.root(),
this.pages_project.project().project_path(),
this.pages_project.ssr_module_context(),
this.pages_project.edge_ssr_module_context(),
this.pages_project.project().ssr_chunking_context(),
@ -660,7 +660,7 @@ impl PageEndpoint {
.project()
.node_root()
.join("server-data".to_string()),
this.path.root(),
this.pages_project.project().project_path(),
this.pages_project.ssr_data_module_context(),
this.pages_project.edge_ssr_data_module_context(),
this.pages_project.project().ssr_data_chunking_context(),
@ -681,7 +681,7 @@ impl PageEndpoint {
.project()
.node_root()
.join("server".to_string()),
this.path.root(),
this.pages_project.project().project_path(),
this.pages_project.ssr_module_context(),
this.pages_project.edge_ssr_module_context(),
this.pages_project.project().ssr_chunking_context(),

View file

@ -190,7 +190,7 @@ pub async fn get_app_entries(
.map(|(pathname, entrypoint)| async move {
Ok(match entrypoint {
Entrypoint::AppPage {
original_name: _,
original_name,
loader_tree,
} => get_app_page_entry(
rsc_context,
@ -199,10 +199,11 @@ pub async fn get_app_entries(
*loader_tree,
app_dir,
pathname.clone(),
original_name.clone(),
project_root,
),
Entrypoint::AppRoute {
original_name: _,
original_name,
path,
} => get_app_route_entry(
rsc_context,
@ -210,6 +211,7 @@ pub async fn get_app_entries(
rsc_context,
Vc::upcast(FileSource::new(*path)),
pathname.clone(),
original_name.clone(),
project_root,
),
})

View file

@ -85,6 +85,7 @@ pub async fn get_app_route_favicon_entry(
Vc::upcast(source),
// TODO(alexkirsz) Get this from the metadata?
"/favicon.ico".to_string(),
"/favicon.ico".to_string(),
project_root,
))
}

View file

@ -6,7 +6,7 @@ use turbopack_binding::{
turbo::tasks_fs::{rope::RopeBuilder, File, FileSystemPath},
turbopack::{
core::{
asset::AssetContent, context::AssetContext, issue::IssueExt, module::Module,
asset::AssetContent, context::AssetContext, issue::IssueExt,
reference_type::ReferenceType, virtual_source::VirtualSource,
},
ecmascript::{chunk::EcmascriptChunkPlaceable, utils::StringifyJs},
@ -22,7 +22,7 @@ use crate::{
next_app::UnsupportedDynamicMetadataIssue,
next_server_component::NextServerComponentTransition,
parse_segment_config_from_loader_tree,
util::{load_next_js, resolve_next_module, NextRuntime},
util::{load_next_js_template, virtual_next_js_template_path, NextRuntime},
};
/// Computes the entry for a Next.js app page.
@ -33,6 +33,7 @@ pub async fn get_app_page_entry(
loader_tree: Vc<LoaderTree>,
app_dir: Vc<FileSystemPath>,
pathname: String,
original_name: String,
project_root: Vc<FileSystemPath>,
) -> Result<Vc<AppEntry>> {
let config = parse_segment_config_from_loader_tree(loader_tree, Vc::upcast(nodejs_context));
@ -77,12 +78,12 @@ pub async fn get_app_page_entry(
let pages = pages.iter().map(|page| page.to_string()).try_join().await?;
let original_name = get_original_page_name(&pathname);
let original_page_name = get_original_page_name(&original_name);
let template_file = "/dist/esm/build/webpack/loaders/next-route-loader/templates/app-page.js";
let template_file = "build/webpack/loaders/next-route-loader/templates/app-page.js";
// Load the file from the next.js codebase.
let file = load_next_js(project_root, template_file).await?.await?;
let file = load_next_js_template(project_root, template_file.to_string()).await?;
let mut file = file
.to_str()?
@ -96,7 +97,7 @@ pub async fn get_app_page_entry(
)
.replace(
"\"VAR_ORIGINAL_PATHNAME\"",
&StringifyJs(&original_name).to_string(),
&StringifyJs(&original_page_name).to_string(),
)
// TODO(alexkirsz) Support custom global error.
.replace(
@ -129,13 +130,7 @@ pub async fn get_app_page_entry(
let file = File::from(result.build());
let resolve_result = resolve_next_module(project_root, template_file).await?;
let Some(template_path) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let template_path = template_path.ident().path();
let template_path = virtual_next_js_template_path(project_root, template_file.to_string());
let source = VirtualSource::new(template_path, AssetContent::file(file.into()));
@ -152,7 +147,7 @@ pub async fn get_app_page_entry(
Ok(AppEntry {
pathname: pathname.to_string(),
original_name,
original_name: original_page_name,
rsc_entry,
config,
}

View file

@ -7,7 +7,6 @@ use turbopack_binding::{
core::{
asset::AssetContent,
context::AssetContext,
module::Module,
reference_type::{
EcmaScriptModulesReferenceSubType, EntryReferenceSubType, ReferenceType,
},
@ -22,7 +21,7 @@ use turbopack_binding::{
use crate::{
next_app::AppEntry,
parse_segment_config_from_source,
util::{load_next_js, resolve_next_module, NextRuntime},
util::{load_next_js_template, virtual_next_js_template_path, NextRuntime},
};
/// Computes the entry for a Next.js app route.
@ -32,6 +31,7 @@ pub async fn get_app_route_entry(
edge_context: Vc<ModuleAssetContext>,
source: Vc<Box<dyn Source>>,
pathname: String,
original_name: String,
project_root: Vc<FileSystemPath>,
) -> Result<Vc<AppEntry>> {
let config = parse_segment_config_from_source(
@ -49,13 +49,13 @@ pub async fn get_app_route_entry(
let mut result = RopeBuilder::default();
let original_name = get_original_route_name(&pathname);
let original_page_name = get_original_route_name(&original_name);
let path = source.ident().path();
let template_file = "/dist/esm/build/webpack/loaders/next-route-loader/templates/app-route.js";
let template_file = "build/webpack/loaders/next-route-loader/templates/app-route.js";
// Load the file from the next.js codebase.
let file = load_next_js(project_root, template_file).await?.await?;
let file = load_next_js_template(project_root, template_file.to_string()).await?;
let mut file = file
.to_str()?
@ -78,7 +78,7 @@ pub async fn get_app_route_entry(
)
.replace(
"\"VAR_ORIGINAL_PATHNAME\"",
&StringifyJs(&original_name).to_string(),
&StringifyJs(&original_page_name).to_string(),
)
.replace(
"\"VAR_RESOLVED_PAGE_PATH\"",
@ -98,13 +98,7 @@ pub async fn get_app_route_entry(
let file = File::from(result.build());
let resolve_result = resolve_next_module(project_root, template_file).await?;
let Some(template_path) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let template_path = template_path.ident().path();
let template_path = virtual_next_js_template_path(project_root, template_file.to_string());
let virtual_source = VirtualSource::new(template_path, AssetContent::file(file.into()));
@ -132,7 +126,7 @@ pub async fn get_app_route_entry(
Ok(AppEntry {
pathname: pathname.to_string(),
original_name,
original_name: original_page_name,
rsc_entry,
config,
}

View file

@ -20,7 +20,7 @@ use crate::{
issue::NextFontIssue,
util::{get_scoped_font_family, FontFamilyType},
},
util::load_next_json,
util::load_next_js_templateon,
};
/// An entry in the Google fonts metrics map
@ -54,8 +54,11 @@ pub(super) async fn get_font_fallback(
Ok(match &options.fallback {
Some(fallback) => FontFallback::Manual(Vc::cell(fallback.clone())).cell(),
None => {
let metrics_json =
load_next_json(context, "/dist/server/capsize-font-metrics.json").await?;
let metrics_json = load_next_js_templateon(
context,
"dist/server/capsize-font-metrics.json".to_string(),
)
.await?;
let fallback = lookup_fallback(
&options.font_family,
metrics_json,

View file

@ -48,7 +48,7 @@ use super::{
get_request_hash, get_request_id, get_scoped_font_family, FontCssProperties, FontFamilyType,
},
};
use crate::{embed_js::next_js_file_path, util::load_next_json};
use crate::{embed_js::next_js_file_path, util::load_next_js_templateon};
pub mod font_fallback;
pub mod options;
@ -266,9 +266,9 @@ impl ImportMappingReplacement for NextFontGoogleCssModuleReplacer {
#[turbo_tasks::function]
async fn load_font_data(project_root: Vc<FileSystemPath>) -> Result<Vc<FontData>> {
let data: FontData = load_next_json(
let data: FontData = load_next_js_templateon(
project_root,
"/dist/compiled/@next/font/dist/google/font-data.json",
"dist/compiled/@next/font/dist/google/font-data.json".to_string(),
)
.await?;

View file

@ -229,6 +229,16 @@ pub async fn get_next_server_import_map(
ServerContextType::AppSSR { .. }
| ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. } => {
match mode {
NextMode::Development | NextMode::Build => {
import_map.insert_wildcard_alias("next/dist/server/", external);
import_map.insert_wildcard_alias("next/dist/shared/", external);
}
NextMode::DevServer => {
// The sandbox can't be bundled and needs to be external
import_map.insert_exact_alias("next/dist/server/web/sandbox", external);
}
}
import_map.insert_exact_alias(
"next/head",
request_to_import_mapping(project_path, "next/dist/client/components/noop-head"),
@ -237,9 +247,6 @@ pub async fn get_next_server_import_map(
"next/dynamic",
request_to_import_mapping(project_path, "next/dist/shared/lib/app-dynamic"),
);
// The sandbox can't be bundled and needs to be external
import_map.insert_exact_alias("next/dist/server/web/sandbox", external);
}
ServerContextType::Middleware => {}
}
@ -620,17 +627,19 @@ async fn package_lookup_resolve_options(
}
#[turbo_tasks::function]
pub async fn get_next_package(project_path: Vc<FileSystemPath>) -> Result<Vc<FileSystemPath>> {
pub async fn get_next_package(context_directory: Vc<FileSystemPath>) -> Result<Vc<FileSystemPath>> {
let result = resolve(
project_path,
context_directory,
Request::parse(Value::new(Pattern::Constant(
"next/package.json".to_string(),
))),
package_lookup_resolve_options(project_path),
package_lookup_resolve_options(context_directory),
);
let assets = result.primary_sources().await?;
let asset = *assets.first().context("Next.js package not found")?;
Ok(asset.ident().path().parent())
let source = result
.first_source()
.await?
.context("Next.js package not found")?;
Ok(source.ident().path().parent())
}
pub async fn insert_alias_option<const N: usize>(

View file

@ -13,7 +13,6 @@ use turbopack_binding::{
core::{
asset::AssetContent,
context::AssetContext,
module::Module,
reference_type::{EntryReferenceSubType, ReferenceType},
source::Source,
virtual_source::VirtualSource,
@ -22,7 +21,7 @@ use turbopack_binding::{
},
};
use crate::util::{load_next_js, resolve_next_module};
use crate::util::{load_next_js_template, virtual_next_js_template_path};
#[turbo_tasks::function]
pub async fn create_page_ssr_entry_module(
@ -43,17 +42,17 @@ pub async fn create_page_ssr_entry_module(
let template_file = match reference_type {
ReferenceType::Entry(EntryReferenceSubType::Page) => {
// Load the Page entry file.
"/dist/esm/build/webpack/loaders/next-route-loader/templates/pages.js"
"build/webpack/loaders/next-route-loader/templates/pages.js"
}
ReferenceType::Entry(EntryReferenceSubType::PagesApi) => {
// Load the Pages API entry file.
"/dist/esm/build/webpack/loaders/next-route-loader/templates/pages-api.js"
"build/webpack/loaders/next-route-loader/templates/pages-api.js"
}
_ => bail!("Invalid path type"),
};
// Load the file from the next.js codebase.
let file = load_next_js(project_root, template_file).await?.await?;
let file = load_next_js_template(project_root, template_file.to_string()).await?;
let mut file = file
.to_str()?
@ -103,13 +102,7 @@ pub async fn create_page_ssr_entry_module(
let file = File::from(result.build());
let resolve_result = resolve_next_module(project_root, template_file).await?;
let Some(template_path) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let template_path = template_path.ident().path();
let template_path = virtual_next_js_template_path(project_root, template_file.to_string());
let source = VirtualSource::new(template_path, AssetContent::file(file.into()));

View file

@ -2,22 +2,16 @@ use anyhow::{bail, Result};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use swc_core::ecma::ast::Program;
use turbo_tasks::{trace::TraceRawVcs, TaskInput, Value, ValueDefault, ValueToString, Vc};
use turbo_tasks::{trace::TraceRawVcs, TaskInput, ValueDefault, ValueToString, Vc};
use turbo_tasks_fs::rope::Rope;
use turbopack_binding::{
turbo::tasks_fs::{json::parse_json_rope_with_source_context, FileContent, FileSystemPath},
turbopack::{
core::{
asset::Asset,
environment::{ServerAddr, ServerInfo},
ident::AssetIdent,
issue::{Issue, IssueExt, IssueSeverity, OptionIssueSource},
issue::{Issue, IssueExt, IssueSeverity},
module::Module,
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
resolve::{
self, handle_resolve_error, node::node_cjs_resolve_options, parse::Request,
ModuleResolveResult,
},
},
ecmascript::{
analyzer::{JsValue, ObjectPart},
@ -28,7 +22,10 @@ use turbopack_binding::{
},
};
use crate::next_config::{NextConfig, OutputType};
use crate::{
next_config::{NextConfig, OutputType},
next_import_map::get_next_package,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, TaskInput)]
pub enum PathType {
@ -333,14 +330,16 @@ fn parse_config_from_js_value(module: Vc<Box<dyn Module>>, value: &JsValue) -> N
config
}
pub async fn load_next_js(context: Vc<FileSystemPath>, path: &str) -> Result<Vc<Rope>> {
let resolve_result = resolve_next_module(context, path).await?;
#[turbo_tasks::function]
pub async fn load_next_js_template(
project_path: Vc<FileSystemPath>,
path: String,
) -> Result<Vc<Rope>> {
let file_path = get_next_package(project_path)
.join("dist/esm".to_string())
.join(path);
let Some(js_asset) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let content = &*js_asset.content().file_content().await?;
let content = &*file_path.read().await?;
let FileContent::Content(file) = content else {
bail!("Expected file content for file");
@ -349,44 +348,23 @@ pub async fn load_next_js(context: Vc<FileSystemPath>, path: &str) -> Result<Vc<
Ok(file.content().to_owned().cell())
}
pub async fn resolve_next_module(
context: Vc<FileSystemPath>,
path: &str,
) -> Result<Vc<ModuleResolveResult>> {
let request = Request::module(
"next".to_owned(),
Value::new(path.to_string().into()),
Vc::cell(None),
);
let resolve_options = node_cjs_resolve_options(context.root());
let resolve_result = handle_resolve_error(
resolve::resolve(context, request, resolve_options).as_raw_module_result(),
Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
)),
context,
request,
resolve_options,
OptionIssueSource::none(),
IssueSeverity::Error.cell(),
)
.await?;
Ok(resolve_result)
#[turbo_tasks::function]
pub fn virtual_next_js_template_path(
project_path: Vc<FileSystemPath>,
path: String,
) -> Vc<FileSystemPath> {
get_next_package(project_path)
.join("dist/esm".to_string())
.join(path)
}
pub async fn load_next_json<T: DeserializeOwned>(
context: Vc<FileSystemPath>,
path: &str,
pub async fn load_next_js_templateon<T: DeserializeOwned>(
project_path: Vc<FileSystemPath>,
path: String,
) -> Result<T> {
let resolve_result = resolve_next_module(context, path).await?;
let file_path = get_next_package(project_path).join(path);
let Some(metrics_asset) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let content = &*metrics_asset.content().file_content().await?;
let content = &*file_path.read().await?;
let FileContent::Content(file) = content else {
bail!("Expected file content for metrics data");

View file

@ -1,7 +1,11 @@
import type { LoaderTree } from '../../../../../server/lib/app-dir-module'
// @ts-ignore this need to be imported from next/dist to be external
import * as module from 'next/dist/server/future/route-modules/app-page/module'
import { RouteKind } from '../../../../../server/future/route-kind'
import { AppPageRouteModule } from '../../../../../server/future/route-modules/app-page/module'
const AppPageRouteModule =
module.AppPageRouteModule as unknown as typeof import('../../../../../server/future/route-modules/app-page/module').AppPageRouteModule
// These are injected by the loader afterwards.
declare const tree: LoaderTree

View file

@ -1,14 +1,16 @@
import '../../../../../server/node-polyfill-headers'
import {
AppRouteRouteModule,
type AppRouteRouteModuleOptions,
} from '../../../../../server/future/route-modules/app-route/module'
// @ts-ignore this need to be imported from next/dist to be external
import * as module from 'next/dist/server/future/route-modules/app-route/module'
import type { AppRouteRouteModuleOptions } from '../../../../../server/future/route-modules/app-route/module'
import { RouteKind } from '../../../../../server/future/route-kind'
// @ts-expect-error - replaced by webpack/turbopack loader
import * as userland from 'VAR_USERLAND'
const AppRouteRouteModule =
module.AppRouteRouteModule as unknown as typeof import('../../../../../server/future/route-modules/app-route/module').AppRouteRouteModule
// These are injected by the loader afterwards. This is injected as a variable
// instead of a replacement because this could also be `undefined` instead of
// an empty string.

View file

@ -1,7 +1,11 @@
import { PagesAPIRouteModule } from '../../../../../server/future/route-modules/pages-api/module'
// @ts-ignore this need to be imported from next/dist to be external
import * as module from 'next/dist/server/future/route-modules/pages-api/module'
import { RouteKind } from '../../../../../server/future/route-kind'
import { hoist } from '../helpers'
const PagesAPIRouteModule =
module.PagesAPIRouteModule as unknown as typeof import('../../../../../server/future/route-modules/pages-api/module').PagesAPIRouteModule
// Import the userland code.
// @ts-expect-error - replaced by webpack/turbopack loader
import * as userland from 'VAR_USERLAND'

View file

@ -1,4 +1,5 @@
import { PagesRouteModule } from '../../../../../server/future/route-modules/pages/module'
// @ts-ignore this need to be imported from next/dist to be external
import * as module from 'next/dist/server/future/route-modules/pages/module'
import { RouteKind } from '../../../../../server/future/route-kind'
import { hoist } from '../helpers'
@ -12,6 +13,9 @@ import App from 'VAR_MODULE_APP'
// @ts-expect-error - replaced by webpack/turbopack loader
import * as userland from 'VAR_USERLAND'
const PagesRouteModule =
module.PagesRouteModule as unknown as typeof import('../../../../../server/future/route-modules/pages/module').PagesRouteModule
// Re-export the component (should be the default export).
export default hoist(userland, 'default')

View file

@ -12,6 +12,9 @@ const { createNextInstall } = require('./test/lib/create-next-install')
const glob = promisify(_glob)
const exec = promisify(execOrig)
const GROUP = process.env.CI ? '##[group]' : ''
const ENDGROUP = process.env.CI ? '##[endgroup]' : ''
// Try to read an external array-based json to filter tests to be allowed / or disallowed.
// If process.argv contains a test to be executed, this'll append it to the list.
const externalTestsFilterLists = process.env.NEXT_EXTERNAL_TESTS_FILTERS
@ -272,7 +275,9 @@ async function main() {
return cleanUpAndExit(1)
}
console.log('Running tests:', '\n', ...testNames.map((name) => `${name}\n`))
console.log(`${GROUP}Running tests:
${testNames.join('\n')}
${ENDGROUP}`)
const hasIsolatedTests = testNames.some((test) => {
return configuredTestTypes.some(
@ -288,7 +293,7 @@ async function main() {
// for isolated next tests: e2e, dev, prod we create
// a starter Next.js install to re-use to speed up tests
// to avoid having to run yarn each time
console.log('Creating Next.js install for isolated tests')
console.log(`${GROUP}Creating Next.js install for isolated tests`)
const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest'
const { installDir, pkgPaths, tmpRepoDir } = await createNextInstall({
parentSpan: mockTrace(),
@ -307,9 +312,11 @@ async function main() {
process.env.NEXT_TEST_PKG_PATHS = JSON.stringify(serializedPkgPaths)
process.env.NEXT_TEST_TEMP_REPO = tmpRepoDir
process.env.NEXT_TEST_STARTER = installDir
console.log(`${ENDGROUP}`)
}
const sema = new Sema(concurrency, { capacity: testNames.length })
const outputSema = new Sema(1, { capacity: testNames.length })
const children = new Set()
const jestPath = path.join(
__dirname,
@ -374,7 +381,7 @@ async function main() {
if (hideOutput) {
outputChunks.push({ type, chunk })
} else {
process.stderr.write(chunk)
process.stdout.write(chunk)
}
}
child.stdout.on('data', handleOutput('stdout'))
@ -386,20 +393,22 @@ async function main() {
children.delete(child)
if (code !== 0 || signal !== null) {
if (hideOutput) {
await outputSema.acquire()
process.stdout.write(`${GROUP}${test} output\n`)
// limit out to last 64kb so that we don't
// run out of log room in CI
outputChunks.forEach(({ type, chunk }) => {
if (type === 'stdout') {
process.stdout.write(chunk)
} else {
process.stderr.write(chunk)
}
})
for (const { chunk } of outputChunks) {
process.stdout.write(chunk)
}
process.stdout.write(`end of ${test} output\n${ENDGROUP}\n`)
outputSema.release()
}
const err = new Error(
code ? `failed with code: ${code}` : `failed with signal: ${signal}`
)
err.output = outputChunks.map((chunk) => chunk.toString()).join('')
err.output = outputChunks
.map(({ chunk }) => chunk.toString())
.join('')
return reject(err)
}
@ -498,11 +507,15 @@ async function main() {
if ((!passed || shouldContinueTestsOnError) && isTestJob) {
try {
const testsOutput = await fs.readFile(`${test}${RESULTS_EXT}`, 'utf8')
await outputSema.acquire()
if (GROUP) console.log(`${GROUP}Result as JSON for tooling`)
console.log(
`--test output start--`,
testsOutput,
`--test output end--`
)
if (ENDGROUP) console.log(ENDGROUP)
outputSema.release()
} catch (err) {
console.log(`Failed to load test output`, err)
}

View file

@ -5,62 +5,64 @@ import { describeVariants as describe } from 'next-test-utils'
import { outdent } from 'outdent'
// TODO-APP: Investigate snapshot mismatch
describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
react: 'latest',
'react-dom': 'latest',
},
skipStart: true,
})
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox app %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
react: 'latest',
'react-dom': 'latest',
},
skipStart: true,
})
// Module trace is only available with webpack 5
test('Node.js builtins', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'node_modules/my-package/index.js',
outdent`
// Module trace is only available with webpack 5
test('Node.js builtins', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'node_modules/my-package/index.js',
outdent`
const dns = require('dns')
module.exports = dns
`,
],
[
'node_modules/my-package/package.json',
outdent`
],
[
'node_modules/my-package/package.json',
outdent`
{
"name": "my-package",
"version": "0.0.1"
}
`,
],
])
)
],
])
)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import pkg from 'my-package'
export default function Hello() {
return (pkg ? <h1>Package loaded</h1> : <h1>Package did not load</h1>)
}
`
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
test('Module not found', async () => {
const { session, cleanup } = await sandbox(next)
test('Module not found', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import Comp from 'b'
export default function Oops() {
return (
@ -70,22 +72,22 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
test('Module not found empty import trace', async () => {
const { session, cleanup } = await sandbox(next)
test('Module not found empty import trace', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'app/page.js',
outdent`
await session.patch(
'app/page.js',
outdent`
'use client'
import Comp from 'b'
export default function Oops() {
@ -96,51 +98,52 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
test('Module not found missing global CSS', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'app/page.js',
outdent`
test('Module not found missing global CSS', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'app/page.js',
outdent`
'use client'
import './non-existent.css'
export default function Page(props) {
return <p>index page</p>
}
`,
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
await session.patch(
'app/page.js',
outdent`
await session.patch(
'app/page.js',
outdent`
'use client'
export default function Page(props) {
return <p>index page</p>
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.documentElement.innerHTML)
).toContain('index page')
)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.documentElement.innerHTML)
).toContain('index page')
await cleanup()
})
})
await cleanup()
})
}
)

File diff suppressed because it is too large Load diff

View file

@ -5,22 +5,24 @@ import { check, describeVariants as describe } from 'next-test-utils'
import path from 'path'
import { outdent } from 'outdent'
describe.each(['default', 'turbo'])('Error recovery app %s', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
react: 'latest',
'react-dom': 'latest',
},
skipStart: true,
})
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'Error recovery app %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
react: 'latest',
'react-dom': 'latest',
},
skipStart: true,
})
test('can recover from a syntax error without losing state', async () => {
const { session, cleanup } = await sandbox(next)
test('can recover from a syntax error without losing state', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
@ -34,23 +36,23 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
)
}
`
)
)
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
await session.patch('index.js', `export default () => <div/`)
await session.patch('index.js', `export default () => <div/`)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'export default () => <div/'
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'export default () => <div/'
)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
@ -64,58 +66,58 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
/Count: 1/
)
await cleanup()
})
test.each([['client'], ['server']])(
'%s component can recover from syntax error',
async (type: string) => {
const { session, browser, cleanup } = await sandbox(
next,
undefined,
'/' + type
)
// Add syntax error
await session.patch(
`app/${type}/page.js`,
outdent`
expect(await session.hasRedbox(false)).toBe(false)
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
/Count: 1/
)
await cleanup()
})
test.each([['client'], ['server']])(
'%s component can recover from syntax error',
async (type: string) => {
const { session, browser, cleanup } = await sandbox(
next,
undefined,
'/' + type
)
// Add syntax error
await session.patch(
`app/${type}/page.js`,
outdent`
export default function Page() {
return <p>Hello world</p>
`
)
expect(await session.hasRedbox(true)).toBe(true)
)
expect(await session.hasRedbox(true)).toBe(true)
// Fix syntax error
await session.patch(
`app/${type}/page.js`,
outdent`
// Fix syntax error
await session.patch(
`app/${type}/page.js`,
outdent`
export default function Page() {
return <p>Hello world 2</p>
}
`
)
)
await check(() => browser.elementByCss('p').text(), 'Hello world 2')
await cleanup()
}
)
await check(() => browser.elementByCss('p').text(), 'Hello world 2')
await cleanup()
}
)
test('can recover from a event handler error', async () => {
const { session, cleanup } = await sandbox(next)
test('can recover from a event handler error', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
@ -132,18 +134,18 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
)
}
`
)
)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('0')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('0')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
await session.waitForAndOpenRuntimeError()
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
await session.waitForAndOpenRuntimeError()
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"index.js (7:10) @ eval
5 | const increment = useCallback(() => {
@ -155,9 +157,9 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
10 | <main>"
`)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
@ -171,46 +173,46 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasErrorToast()).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 1')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 2')
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasErrorToast()).toBe(false)
await cleanup()
})
test.each([['client'], ['server']])(
'%s component can recover from a component error',
async (type: string) => {
const { session, cleanup, browser } = await sandbox(
next,
undefined,
'/' + type
)
await session.write(
'child.js',
outdent`
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasErrorToast()).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 1')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 2')
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasErrorToast()).toBe(false)
await cleanup()
})
test.each([['client'], ['server']])(
'%s component can recover from a component error',
async (type: string) => {
const { session, cleanup, browser } = await sandbox(
next,
undefined,
'/' + type
)
await session.write(
'child.js',
outdent`
export default function Child() {
return <p>Hello</p>;
}
`
)
)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import Child from './child'
export default function Index() {
@ -221,121 +223,121 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
)
}
`
)
)
expect(await browser.elementByCss('p').text()).toBe('Hello')
expect(await browser.elementByCss('p').text()).toBe('Hello')
await session.patch(
'child.js',
outdent`
await session.patch(
'child.js',
outdent`
// hello
export default function Child() {
throw new Error('oops')
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'export default function Child()'
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'export default function Child()'
)
// TODO-APP: re-enable when error recovery doesn't reload the page.
/* const didNotReload = */ await session.patch(
'child.js',
outdent`
// TODO-APP: re-enable when error recovery doesn't reload the page.
/* const didNotReload = */ await session.patch(
'child.js',
outdent`
export default function Child() {
return <p>Hello</p>;
}
`
)
// TODO-APP: re-enable when error recovery doesn't reload the page.
// expect(didNotReload).toBe(true)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello')
await cleanup()
}
)
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554150098
test('syntax > runtime error', async () => {
const { session, cleanup } = await sandbox(next)
// Start here.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
export default function FunctionNamed() {
return <div />
}
`
)
// TODO: this acts weird without above step
await session.patch(
'index.js',
outdent`
import * as React from 'react';
let i = 0
setInterval(() => {
i++
throw Error('no ' + i)
}, 1000)
export default function FunctionNamed() {
return <div />
}
`
)
// TODO-APP: re-enable when error recovery doesn't reload the page.
// expect(didNotReload).toBe(true)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello')
await new Promise((resolve) => setTimeout(resolve, 1000))
await session.waitForAndOpenRuntimeError()
expect(await session.getRedboxSource()).not.toInclude(
"Expected '}', got '<eof>'"
)
// Make a syntax error.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
let i = 0
setInterval(() => {
i++
throw Error('no ' + i)
}, 1000)
export default function FunctionNamed() {
`
)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
"Expected '}', got '<eof>'"
)
// Test that runtime error does not take over:
await new Promise((resolve) => setTimeout(resolve, 2000))
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
"Expected '}', got '<eof>'"
)
await cleanup()
}
)
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554150098
test('syntax > runtime error', async () => {
const { session, cleanup } = await sandbox(next)
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554144016
test('stuck error', async () => {
const { session, cleanup } = await sandbox(next)
// Start here.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
export default function FunctionNamed() {
return <div />
}
`
)
// TODO: this acts weird without above step
await session.patch(
'index.js',
outdent`
import * as React from 'react';
let i = 0
setInterval(() => {
i++
throw Error('no ' + i)
}, 1000)
export default function FunctionNamed() {
return <div />
}
`
)
await new Promise((resolve) => setTimeout(resolve, 1000))
await session.waitForAndOpenRuntimeError()
expect(await session.getRedboxSource()).not.toInclude(
"Expected '}', got '<eof>'"
)
// Make a syntax error.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
let i = 0
setInterval(() => {
i++
throw Error('no ' + i)
}, 1000)
export default function FunctionNamed() {
`
)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
"Expected '}', got '<eof>'"
)
// Test that runtime error does not take over:
await new Promise((resolve) => setTimeout(resolve, 2000))
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
"Expected '}', got '<eof>'"
)
await cleanup()
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554144016
test('stuck error', async () => {
const { session, cleanup } = await sandbox(next)
// We start here.
await session.patch(
'index.js',
outdent`
// We start here.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
function FunctionDefault() {
@ -344,23 +346,23 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
export default FunctionDefault;
`
)
)
// We add a new file. Let's call it Foo.js.
await session.write(
'Foo.js',
outdent`
// We add a new file. Let's call it Foo.js.
await session.write(
'Foo.js',
outdent`
// intentionally skips export
export default function Foo() {
return React.createElement('h1', null, 'Foo');
}
`
)
)
// We edit our first file to use it.
await session.patch(
'index.js',
outdent`
// We edit our first file to use it.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
import Foo from './Foo';
function FunctionDefault() {
@ -368,39 +370,39 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
}
export default FunctionDefault;
`
)
)
// We get an error because Foo didn't import React. Fair.
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
"return React.createElement('h1', null, 'Foo');"
)
// We get an error because Foo didn't import React. Fair.
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
"return React.createElement('h1', null, 'Foo');"
)
// Let's add that to Foo.
await session.patch(
'Foo.js',
outdent`
// Let's add that to Foo.
await session.patch(
'Foo.js',
outdent`
import * as React from 'react';
export default function Foo() {
return React.createElement('h1', null, 'Foo');
}
`
)
)
// Expected: this fixes the problem
expect(await session.hasRedbox(false)).toBe(false)
// Expected: this fixes the problem
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
await cleanup()
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137262
test('render error not shown right after syntax error', async () => {
const { session, cleanup } = await sandbox(next)
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137262
test('render error not shown right after syntax error', async () => {
const { session, cleanup } = await sandbox(next)
// Starting here:
await session.patch(
'index.js',
outdent`
// Starting here:
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
render() {
@ -410,16 +412,16 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
export default ClassDefault;
`
)
)
expect(
await session.evaluate(() => document.querySelector('h1').textContent)
).toBe('Default Export')
expect(
await session.evaluate(() => document.querySelector('h1').textContent)
).toBe('Default Export')
// Break it with a syntax error:
await session.patch(
'index.js',
outdent`
// Break it with a syntax error:
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
@ -430,13 +432,13 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
export default ClassDefault;
`
)
expect(await session.hasRedbox(true)).toBe(true)
)
expect(await session.hasRedbox(true)).toBe(true)
// Now change the code to introduce a runtime error without fixing the syntax error:
await session.patch(
'index.js',
outdent`
// Now change the code to introduce a runtime error without fixing the syntax error:
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
@ -448,13 +450,13 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
export default ClassDefault;
`
)
expect(await session.hasRedbox(true)).toBe(true)
)
expect(await session.hasRedbox(true)).toBe(true)
// Now fix the syntax error:
await session.patch(
'index.js',
outdent`
// Now fix the syntax error:
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
@ -466,30 +468,31 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
export default ClassDefault;
`
)
expect(await session.hasRedbox(true)).toBe(true)
)
expect(await session.hasRedbox(true)).toBe(true)
await check(async () => {
const source = await session.getRedboxSource()
return source?.includes('render() {') ? 'success' : source
}, 'success')
await check(async () => {
const source = await session.getRedboxSource()
return source?.includes('render() {') ? 'success' : source
}, 'success')
expect(await session.getRedboxSource()).toInclude(
"throw new Error('nooo');"
)
expect(await session.getRedboxSource()).toInclude(
"throw new Error('nooo');"
)
await cleanup()
})
await cleanup()
})
test('displays build error on initial page load', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([['app/page.js', '{{{']])
)
test('displays build error on initial page load', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([['app/page.js', '{{{']])
)
expect(await session.hasRedbox(true)).toBe(true)
await check(() => session.getRedboxSource(true), /Failed to compile/)
expect(await session.hasRedbox(true)).toBe(true)
await check(() => session.getRedboxSource(true), /Failed to compile/)
await cleanup()
})
})
await cleanup()
})
}
)

View file

@ -4,48 +4,50 @@ import { describeVariants as describe } from 'next-test-utils'
import { outdent } from 'outdent'
import path from 'path'
describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
test('empty _app shows logbox', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([['pages/_app.js', ``]])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: The default export is not a React Component in page: \\"/_app\\""`
)
test('empty _app shows logbox', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([['pages/_app.js', ``]])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: The default export is not a React Component in page: \\"/_app\\""`
)
await session.patch(
'pages/_app.js',
outdent`
await session.patch(
'pages/_app.js',
outdent`
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp
`
)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
test('empty _document shows logbox', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([['pages/_document.js', ``]])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: The default export is not a React Component in page: \\"/_document\\""`
)
test('empty _document shows logbox', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([['pages/_document.js', ``]])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: The default export is not a React Component in page: \\"/_document\\""`
)
await session.patch(
'pages/_document.js',
outdent`
await session.patch(
'pages/_document.js',
outdent`
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
@ -69,31 +71,31 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
export default MyDocument
`
)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
test('_app syntax error shows logbox', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'pages/_app.js',
outdent`
test('_app syntax error shows logbox', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'pages/_app.js',
outdent`
function MyApp({ Component, pageProps }) {
return <<Component {...pageProps} />;
}
export default MyApp
`,
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
"./pages/_app.js
Error:
x Expression expected
@ -117,28 +119,28 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
Caused by:
Syntax Error"
`)
)
)
await session.patch(
'pages/_app.js',
outdent`
await session.patch(
'pages/_app.js',
outdent`
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp
`
)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
test('_document syntax error shows logbox', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'pages/_document.js',
outdent`
test('_document syntax error shows logbox', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'pages/_document.js',
outdent`
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {{
@ -162,14 +164,14 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
export default MyDocument
`,
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
"./pages/_document.js
Error:
x Unexpected token \`{\`. Expected identifier, string literal, numeric literal or [ for the computed key
@ -186,11 +188,11 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
Caused by:
Syntax Error"
`)
)
)
await session.patch(
'pages/_document.js',
outdent`
await session.patch(
'pages/_document.js',
outdent`
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
@ -214,8 +216,9 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
export default MyDocument
`
)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
})
)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
}
)

View file

@ -4,58 +4,60 @@ import { describeVariants as describe } from 'next-test-utils'
import { outdent } from 'outdent'
import path from 'path'
describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
// Module trace is only available with webpack 5
test('Node.js builtins', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'node_modules/my-package/index.js',
outdent`
// Module trace is only available with webpack 5
test('Node.js builtins', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'node_modules/my-package/index.js',
outdent`
const dns = require('dns')
module.exports = dns
`,
],
[
'node_modules/my-package/package.json',
outdent`
],
[
'node_modules/my-package/package.json',
outdent`
{
"name": "my-package",
"version": "0.0.1"
}
`,
],
])
)
],
])
)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import pkg from 'my-package'
export default function Hello() {
return (pkg ? <h1>Package loaded</h1> : <h1>Package did not load</h1>)
}
`
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
test('Module not found', async () => {
const { session, cleanup } = await sandbox(next)
test('Module not found', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import Comp from 'b'
export default function Oops() {
@ -66,22 +68,22 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
test('Module not found (empty import trace)', async () => {
const { session, cleanup } = await sandbox(next)
test('Module not found (empty import trace)', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'pages/index.js',
outdent`
await session.patch(
'pages/index.js',
outdent`
import Comp from 'b'
export default function Oops() {
@ -92,58 +94,59 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
test('Module not found (missing global CSS)', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'pages/_app.js',
outdent`
test('Module not found (missing global CSS)', async () => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'pages/_app.js',
outdent`
import './non-existent.css'
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
`,
],
[
'pages/index.js',
outdent`
],
[
'pages/index.js',
outdent`
export default function Page(props) {
return <p>index page</p>
}
`,
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
await session.patch(
'pages/_app.js',
outdent`
await session.patch(
'pages/_app.js',
outdent`
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.documentElement.innerHTML)
).toContain('index page')
)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.documentElement.innerHTML)
).toContain('index page')
await cleanup()
})
})
await cleanup()
})
}
)

View file

@ -5,18 +5,20 @@ import { describeVariants as describe } from 'next-test-utils'
import path from 'path'
import { outdent } from 'outdent'
describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
test('should strip whitespace correctly with newline', async () => {
const { session, cleanup } = await sandbox(next)
test('should strip whitespace correctly with newline', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
export default function Page() {
return (
<>
@ -32,24 +34,24 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
await session.evaluate(() => document.querySelector('a').click())
)
await session.evaluate(() => document.querySelector('a').click())
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137807
test('module init error not shown', async () => {
// Start here:
const { session, cleanup } = await sandbox(next)
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137807
test('module init error not shown', async () => {
// Start here:
const { session, cleanup } = await sandbox(next)
// We start here.
await session.patch(
'index.js',
outdent`
// We start here.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
render() {
@ -58,16 +60,16 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
}
export default ClassDefault;
`
)
)
expect(
await session.evaluate(() => document.querySelector('h1').textContent)
).toBe('Default Export')
expect(
await session.evaluate(() => document.querySelector('h1').textContent)
).toBe('Default Export')
// Add a throw in module init phase:
await session.patch(
'index.js',
outdent`
// Add a throw in module init phase:
await session.patch(
'index.js',
outdent`
// top offset for snapshot
import * as React from 'react';
throw new Error('no')
@ -78,29 +80,29 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
}
export default ClassDefault;
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554152127
test('boundaries', async () => {
const { session, cleanup } = await sandbox(next)
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554152127
test('boundaries', async () => {
const { session, cleanup } = await sandbox(next)
await session.write(
'FunctionDefault.js',
outdent`
await session.write(
'FunctionDefault.js',
outdent`
export default function FunctionDefault() {
return <h2>hello</h2>
}
`
)
await session.patch(
'index.js',
outdent`
)
await session.patch(
'index.js',
outdent`
import FunctionDefault from './FunctionDefault.js'
import * as React from 'react'
class ErrorBoundary extends React.Component {
@ -130,58 +132,58 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
}
export default App;
`
)
)
expect(
await session.evaluate(() => document.querySelector('h2').textContent)
).toBe('hello')
expect(
await session.evaluate(() => document.querySelector('h2').textContent)
).toBe('hello')
await session.write(
'FunctionDefault.js',
`export default function FunctionDefault() { throw new Error('no'); }`
)
await session.write(
'FunctionDefault.js',
`export default function FunctionDefault() { throw new Error('no'); }`
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
expect(
await session.evaluate(() => document.querySelector('h2').textContent)
).toBe('error')
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
expect(
await session.evaluate(() => document.querySelector('h2').textContent)
).toBe('error')
await cleanup()
})
await cleanup()
})
// TODO: investigate why this fails when running outside of the Next.js
// monorepo e.g. fails when using yarn create next-app
// https://github.com/vercel/next.js/pull/23203
test.skip('internal package errors', async () => {
const { session, cleanup } = await sandbox(next)
// TODO: investigate why this fails when running outside of the Next.js
// monorepo e.g. fails when using yarn create next-app
// https://github.com/vercel/next.js/pull/23203
test.skip('internal package errors', async () => {
const { session, cleanup } = await sandbox(next)
// Make a react build-time error.
await session.patch(
'index.js',
outdent`
// Make a react build-time error.
await session.patch(
'index.js',
outdent`
export default function FunctionNamed() {
return <div>{{}}</div>
}`
)
)
expect(await session.hasRedbox(true)).toBe(true)
// We internally only check the script path, not including the line number
// and error message because the error comes from an external library.
// This test ensures that the errored script path is correctly resolved.
expect(await session.getRedboxSource()).toContain(
`../../../../packages/next/dist/pages/_document.js`
)
expect(await session.hasRedbox(true)).toBe(true)
// We internally only check the script path, not including the line number
// and error message because the error comes from an external library.
// This test ensures that the errored script path is correctly resolved.
expect(await session.getRedboxSource()).toContain(
`../../../../packages/next/dist/pages/_document.js`
)
await cleanup()
})
await cleanup()
})
test('unterminated JSX', async () => {
const { session, cleanup } = await sandbox(next)
test('unterminated JSX', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>
@ -190,13 +192,13 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasRedbox(false)).toBe(false)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>
@ -205,13 +207,13 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(next.normalizeTestDirContent(source)).toMatchInlineSnapshot(
next.normalizeSnapshot(`
const source = await session.getRedboxSource()
expect(next.normalizeTestDirContent(source)).toMatchInlineSnapshot(
next.normalizeSnapshot(`
"./index.js
Error:
x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
@ -238,27 +240,27 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
./index.js
./pages/index.js"
`)
)
)
await cleanup()
})
await cleanup()
})
// Module trace is only available with webpack 5
test('conversion to class component (1)', async () => {
const { session, cleanup } = await sandbox(next)
// Module trace is only available with webpack 5
test('conversion to class component (1)', async () => {
const { session, cleanup } = await sandbox(next)
await session.write(
'Child.js',
outdent`
await session.write(
'Child.js',
outdent`
export default function ClickCount() {
return <p>hello</p>
}
`
)
)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import Child from './Child';
export default function Home() {
@ -269,16 +271,16 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('hello')
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('hello')
await session.patch(
'Child.js',
outdent`
await session.patch(
'Child.js',
outdent`
import { Component } from 'react';
export default class ClickCount extends Component {
render() {
@ -286,14 +288,14 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
}
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchSnapshot()
await session.patch(
'Child.js',
outdent`
await session.patch(
'Child.js',
outdent`
import { Component } from 'react';
export default class ClickCount extends Component {
render() {
@ -301,23 +303,23 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
}
}
`
)
)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('hello new')
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('hello new')
await cleanup()
})
await cleanup()
})
test('css syntax errors', async () => {
const { session, cleanup } = await sandbox(next)
test('css syntax errors', async () => {
const { session, cleanup } = await sandbox(next)
await session.write('index.module.css', `.button {}`)
await session.patch(
'index.js',
outdent`
await session.write('index.module.css', `.button {}`)
await session.patch(
'index.js',
outdent`
import './index.module.css';
export default () => {
return (
@ -327,35 +329,35 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasRedbox(false)).toBe(false)
// Syntax error
await session.patch('index.module.css', `.button {`)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatch('./index.module.css:1:1')
expect(source).toMatch('Syntax error: ')
expect(source).toMatch('Unclosed block')
expect(source).toMatch('> 1 | .button {')
expect(source).toMatch(' | ^')
// Syntax error
await session.patch('index.module.css', `.button {`)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatch('./index.module.css:1:1')
expect(source).toMatch('Syntax error: ')
expect(source).toMatch('Unclosed block')
expect(source).toMatch('> 1 | .button {')
expect(source).toMatch(' | ^')
// Not local error
await session.patch('index.module.css', `button {}`)
expect(await session.hasRedbox(true)).toBe(true)
const source2 = await session.getRedboxSource()
expect(source2).toMatchSnapshot()
// Not local error
await session.patch('index.module.css', `button {}`)
expect(await session.hasRedbox(true)).toBe(true)
const source2 = await session.getRedboxSource()
expect(source2).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
test('logbox: anchors links in error messages', async () => {
const { session, cleanup } = await sandbox(next)
test('logbox: anchors links in error messages', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
@ -369,39 +371,39 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header = await session.getRedboxDescription()
expect(header).toMatchSnapshot()
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header = await session.getRedboxDescription()
expect(header).toMatchSnapshot()
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
@ -415,39 +417,39 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header2 = await session.getRedboxDescription()
expect(header2).toMatchSnapshot()
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header2 = await session.getRedboxDescription()
expect(header2).toMatchSnapshot()
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
@ -461,39 +463,39 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header3 = await session.getRedboxDescription()
expect(header3).toMatchSnapshot()
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header3 = await session.getRedboxDescription()
expect(header3).toMatchSnapshot()
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
@ -507,53 +509,53 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header4 = await session.getRedboxDescription()
expect(header4).toMatchInlineSnapshot(
`"Error: multiple http://nextjs.org links http://example.com"`
)
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(2)
expect(
await session.evaluate(
() =>
(
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header4 = await session.getRedboxDescription()
expect(header4).toMatchInlineSnapshot(
`"Error: multiple http://nextjs.org links http://example.com"`
)
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(2)'
) as any
).href
)
).toMatchSnapshot()
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(2)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(2)'
) as any
).href
)
).toMatchSnapshot()
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
@ -567,59 +569,59 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header5 = await session.getRedboxDescription()
expect(header5).toMatchInlineSnapshot(
`"Error: multiple http://nextjs.org links (http://example.com)"`
)
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(2)
expect(
await session.evaluate(
() =>
(
expect(await session.hasRedbox(false)).toBe(false)
await session.evaluate(() => document.querySelector('button').click())
expect(await session.hasRedbox(true)).toBe(true)
const header5 = await session.getRedboxDescription()
expect(header5).toMatchInlineSnapshot(
`"Error: multiple http://nextjs.org links (http://example.com)"`
)
expect(
await session.evaluate(
() =>
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(2)'
) as any
).href
)
).toMatchSnapshot()
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(2)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchSnapshot()
expect(
await session.evaluate(
() =>
(
document
.querySelector('body > nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(2)'
) as any
).href
)
).toMatchSnapshot()
await cleanup()
})
await cleanup()
})
test('non-Error errors are handled properly', async () => {
const { session, cleanup } = await sandbox(next)
test('non-Error errors are handled properly', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
export default () => {
throw {'a': 1, 'b': 'x'};
return (
@ -627,28 +629,28 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: {\\"a\\":1,\\"b\\":\\"x\\"}"`
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: {\\"a\\":1,\\"b\\":\\"x\\"}"`
)
// fix previous error
await session.patch(
'index.js',
outdent`
// fix previous error
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>hello</div>
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.patch(
'index.js',
outdent`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.patch(
'index.js',
outdent`
class Hello {}
export default () => {
@ -658,27 +660,27 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toContain(
`Error: class Hello {`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toContain(
`Error: class Hello {`
)
// fix previous error
await session.patch(
'index.js',
outdent`
// fix previous error
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>hello</div>
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.patch(
'index.js',
outdent`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.patch(
'index.js',
outdent`
export default () => {
throw "string error"
return (
@ -686,27 +688,27 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: string error"`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: string error"`
)
// fix previous error
await session.patch(
'index.js',
outdent`
// fix previous error
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>hello</div>
)
}
`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.patch(
'index.js',
outdent`
)
expect(await session.hasRedbox(false)).toBe(false)
await session.patch(
'index.js',
outdent`
export default () => {
throw null
return (
@ -714,12 +716,13 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toContain(
`Error: A null error was thrown`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toContain(
`Error: A null error was thrown`
)
await cleanup()
})
})
await cleanup()
})
}
)

View file

@ -5,18 +5,20 @@ import { check, describeVariants as describe } from 'next-test-utils'
import { outdent } from 'outdent'
import path from 'path'
describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
test('logbox: can recover from a syntax error without losing state', async () => {
const { session, cleanup } = await sandbox(next)
test('logbox: can recover from a syntax error without losing state', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
@ -30,23 +32,23 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
await session.patch('index.js', `export default () => <div/`)
await session.patch('index.js', `export default () => <div/`)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'export default () => <div/'
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'export default () => <div/'
)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
@ -60,24 +62,24 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
/Count: 1/
)
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
/Count: 1/
)
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
await cleanup()
})
test('logbox: can recover from a event handler error', async () => {
const { session, cleanup } = await sandbox(next)
test('logbox: can recover from a event handler error', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
@ -94,18 +96,18 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('0')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('0')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"index.js (7:10) @ eval
5 | const increment = useCallback(() => {
@ -117,9 +119,9 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
10 | <main>"
`)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
@ -133,38 +135,38 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 1')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 2')
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 1')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 2')
expect(await session.hasRedbox(false)).toBe(false)
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
await cleanup()
})
test('logbox: can recover from a component error', async () => {
const { session, cleanup } = await sandbox(next)
test('logbox: can recover from a component error', async () => {
const { session, cleanup } = await sandbox(next)
await session.write(
'child.js',
outdent`
await session.write(
'child.js',
outdent`
export default function Child() {
return <p>Hello</p>;
}
`
)
)
await session.patch(
'index.js',
outdent`
await session.patch(
'index.js',
outdent`
import Child from './child'
export default function Index() {
@ -175,53 +177,53 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
)
}
`
)
)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello')
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello')
await session.patch(
'child.js',
outdent`
await session.patch(
'child.js',
outdent`
// hello
export default function Child() {
throw new Error('oops')
}
`
)
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'export default function Child()'
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'export default function Child()'
)
const didNotReload = await session.patch(
'child.js',
outdent`
const didNotReload = await session.patch(
'child.js',
outdent`
export default function Child() {
return <p>Hello</p>;
}
`
)
)
expect(didNotReload).toBe(true)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello')
expect(didNotReload).toBe(true)
expect(await session.hasRedbox(false)).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello')
await cleanup()
})
await cleanup()
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137262
test('render error not shown right after syntax error', async () => {
const { session, cleanup } = await sandbox(next)
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137262
test('render error not shown right after syntax error', async () => {
const { session, cleanup } = await sandbox(next)
// Starting here:
await session.patch(
'index.js',
outdent`
// Starting here:
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
render() {
@ -231,16 +233,16 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
export default ClassDefault;
`
)
)
expect(
await session.evaluate(() => document.querySelector('h1').textContent)
).toBe('Default Export')
expect(
await session.evaluate(() => document.querySelector('h1').textContent)
).toBe('Default Export')
// Break it with a syntax error:
await session.patch(
'index.js',
outdent`
// Break it with a syntax error:
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
@ -251,13 +253,13 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
export default ClassDefault;
`
)
expect(await session.hasRedbox(true)).toBe(true)
)
expect(await session.hasRedbox(true)).toBe(true)
// Now change the code to introduce a runtime error without fixing the syntax error:
await session.patch(
'index.js',
outdent`
// Now change the code to introduce a runtime error without fixing the syntax error:
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
@ -269,13 +271,13 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
export default ClassDefault;
`
)
expect(await session.hasRedbox(true)).toBe(true)
)
expect(await session.hasRedbox(true)).toBe(true)
// Now fix the syntax error:
await session.patch(
'index.js',
outdent`
// Now fix the syntax error:
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
@ -287,29 +289,29 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
export default ClassDefault;
`
)
expect(await session.hasRedbox(true)).toBe(true)
)
expect(await session.hasRedbox(true)).toBe(true)
await check(async () => {
const source = await session.getRedboxSource()
return source?.includes('render() {') ? 'success' : source
}, 'success')
await check(async () => {
const source = await session.getRedboxSource()
return source?.includes('render() {') ? 'success' : source
}, 'success')
expect(await session.getRedboxSource()).toInclude(
"throw new Error('nooo');"
)
expect(await session.getRedboxSource()).toInclude(
"throw new Error('nooo');"
)
await cleanup()
})
await cleanup()
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554144016
test('stuck error', async () => {
const { session, cleanup } = await sandbox(next)
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554144016
test('stuck error', async () => {
const { session, cleanup } = await sandbox(next)
// We start here.
await session.patch(
'index.js',
outdent`
// We start here.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
function FunctionDefault() {
@ -318,23 +320,23 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
export default FunctionDefault;
`
)
)
// We add a new file. Let's call it Foo.js.
await session.write(
'Foo.js',
outdent`
// We add a new file. Let's call it Foo.js.
await session.write(
'Foo.js',
outdent`
// intentionally skips export
export default function Foo() {
return React.createElement('h1', null, 'Foo');
}
`
)
)
// We edit our first file to use it.
await session.patch(
'index.js',
outdent`
// We edit our first file to use it.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
import Foo from './Foo';
function FunctionDefault() {
@ -342,50 +344,50 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
}
export default FunctionDefault;
`
)
)
// We get an error because Foo didn't import React. Fair.
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
"return React.createElement('h1', null, 'Foo');"
)
// We get an error because Foo didn't import React. Fair.
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
"return React.createElement('h1', null, 'Foo');"
)
// Let's add that to Foo.
await session.patch(
'Foo.js',
outdent`
// Let's add that to Foo.
await session.patch(
'Foo.js',
outdent`
import * as React from 'react';
export default function Foo() {
return React.createElement('h1', null, 'Foo');
}
`
)
)
// Expected: this fixes the problem
expect(await session.hasRedbox(false)).toBe(false)
// Expected: this fixes the problem
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
await cleanup()
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554150098
test('syntax > runtime error', async () => {
const { session, cleanup } = await sandbox(next)
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554150098
test('syntax > runtime error', async () => {
const { session, cleanup } = await sandbox(next)
// Start here.
await session.patch(
'index.js',
outdent`
// Start here.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
export default function FunctionNamed() {
return <div />
}
`
)
// TODO: this acts weird without above step
await session.patch(
'index.js',
outdent`
)
// TODO: this acts weird without above step
await session.patch(
'index.js',
outdent`
import * as React from 'react';
let i = 0
setInterval(() => {
@ -396,20 +398,20 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
return <div />
}
`
)
)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await session.hasRedbox(true)).toBe(true)
if (process.platform === 'win32') {
expect(await session.getRedboxSource()).toMatchSnapshot()
} else {
expect(await session.getRedboxSource()).toMatchSnapshot()
}
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await session.hasRedbox(true)).toBe(true)
if (process.platform === 'win32') {
expect(await session.getRedboxSource()).toMatchSnapshot()
} else {
expect(await session.getRedboxSource()).toMatchSnapshot()
}
// Make a syntax error.
await session.patch(
'index.js',
outdent`
// Make a syntax error.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
let i = 0
setInterval(() => {
@ -417,14 +419,14 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
throw Error('no ' + i)
}, 1000)
export default function FunctionNamed() {`
)
)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
"./index.js
Error:
x Expected '}', got '<eof>'
@ -443,15 +445,15 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
./index.js
./pages/index.js"
`)
)
)
// Test that runtime error does not take over:
await new Promise((resolve) => setTimeout(resolve, 2000))
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
// Test that runtime error does not take over:
await new Promise((resolve) => setTimeout(resolve, 2000))
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
"./index.js
Error:
x Expected '}', got '<eof>'
@ -470,8 +472,9 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
./index.js
./pages/index.js"
`)
)
)
await cleanup()
})
})
await cleanup()
})
}
)

View file

@ -26,7 +26,10 @@ import type { RequestInit, Response } from 'node-fetch'
import type { NextServer } from 'next/dist/server/next'
import type { BrowserInterface } from './browsers/base'
import { shouldRunTurboDevTest } from './turbo'
import {
shouldRunExperimentalTurboDevTest,
shouldRunTurboDevTest,
} from './turbo'
export { shouldRunTurboDevTest }
@ -327,6 +330,7 @@ export interface NextDevOptions {
bootupMarker?: RegExp
nextStart?: boolean
turbo?: boolean
experimentalTurbo?: boolean
stderr?: false
stdout?: false
@ -374,12 +378,19 @@ export function runNextCommandDev(
const bootupMarkers = {
dev: /compiled .*successfully/i,
turbo: /started server/i,
experimentalTurbo: /started server/i,
start: /started server/i,
}
if (
(opts.bootupMarker && opts.bootupMarker.test(message)) ||
bootupMarkers[
opts.nextStart || stdOut ? 'start' : opts?.turbo ? 'turbo' : 'dev'
opts.nextStart || stdOut
? 'start'
: opts?.experimentalTurbo
? 'experimentalTurbo'
: opts?.turbo
? 'turbo'
: 'dev'
].test(message)
) {
if (!didResolve) {
@ -434,6 +445,7 @@ export function launchApp(
) {
const options = opts ?? {}
const useTurbo = shouldRunTurboDevTest()
const useExperimentalTurbo = shouldRunExperimentalTurboDevTest()
return runNextCommandDev(
[useTurbo ? '--turbo' : undefined, dir, '-p', port as string].filter(
@ -443,6 +455,7 @@ export function launchApp(
{
...options,
turbo: useTurbo,
experimentalTurbo: useExperimentalTurbo,
}
)
}
@ -1008,23 +1021,30 @@ export function findAllTelemetryEvents(output: string, eventName: string) {
return events.filter((e) => e.eventName === eventName).map((e) => e.payload)
}
type TestVariants = 'default' | 'turbo'
type TestVariants = 'default' | 'turbo' | 'experimentalTurbo'
// WEB-168: There are some differences / incompletes in turbopack implementation enforces jest requires to update
// test snapshot when run against turbo. This fn returns describe, or describe.skip dependes on the running context
// to avoid force-snapshot update per each runs until turbopack update includes all the changes.
export function getSnapshotTestDescribe(variant: TestVariants) {
const runningEnv = variant ?? 'default'
if (runningEnv !== 'default' && runningEnv !== 'turbo') {
if (
runningEnv !== 'default' &&
runningEnv !== 'turbo' &&
runningEnv !== 'experimentalTurbo'
) {
throw new Error(
`An invalid test env was passed: ${variant} (only "default" and "turbo" are valid options)`
`An invalid test env was passed: ${variant} (only "default", "turbo" and "experimentalTurbo" are valid options)`
)
}
const shouldRunTurboDev = shouldRunTurboDevTest()
const shouldRunExperimentalTurboDev = shouldRunExperimentalTurboDevTest()
const shouldSkip =
(runningEnv === 'turbo' && !shouldRunTurboDev) ||
(runningEnv === 'default' && shouldRunTurboDev)
(runningEnv === 'experimentalTurbo' && !shouldRunExperimentalTurboDev) ||
(runningEnv === 'default' &&
(shouldRunTurboDev || shouldRunExperimentalTurboDev))
return shouldSkip ? describe.skip : describe
}

View file

@ -1,4 +1,5 @@
let loggedTurbopack = false
let loggedExperimentalTurbopack = false
/**
* Utility function to determine if a given test case needs to run with --turbo.
@ -24,3 +25,28 @@ export function shouldRunTurboDevTest(): boolean {
return shouldRunTurboDev
}
/**
* Utility function to determine if a given test case needs to run with --experimental-turbo.
*
* This is primarily for the gradual test enablement with latest turbopack upstream changes.
*
* Note: it could be possible to dynamically create test cases itself (createDevTest(): it.each([...])), but
* it makes hard to conform with existing lint rules. Instead, starting off from manual fixture setup and
* update test cases accordingly as turbopack changes enable more test cases.
*/
export function shouldRunExperimentalTurboDevTest(): boolean {
if (!!process.env.TEST_WASM) {
return false
}
const shouldRunExperimentalTurboDev = !!process.env.EXPERIMENTAL_TURBOPACK
if (shouldRunExperimentalTurboDev && !loggedExperimentalTurbopack) {
require('console').log(
`Running tests with experimental turbopack because environment variable EXPERIMENTAL_TURBOPACK is set`
)
loggedExperimentalTurbopack = true
}
return shouldRunExperimentalTurboDev
}

View file

@ -0,0 +1,9 @@
// Tests that are currently enabled with experimental Turbopack in CI.
// Only tests that are actively testing against Turbopack should
// be enabled here
const enabledTests = [
'test/development/api-cors-with-rewrite/index.test.ts',
'test/integration/bigint/test/index.test.js',
]
module.exports = { enabledTests }