Basic @next/font/google integration test (vercel/turbo#3170)

This:

* Implements a basic http server to mock returning a stylesheet from the Google Fonts API (**Note**: This importantly does *not* mock returning the font resources themselves, which are currently loaded by the browser. We should implement something to do this when we implement caching arbitrary http resources traced from `url()` and this is no longer loaded by the test browser)
* Adds an integration test that asserts the basic shape of the JS object returned by font functions
* ~Adds an integration test that asserts the browser correctly loads a font for the ascii unicode range, along with the appropriate `font-display`, variant, etc.~ Unfortunately `document.fonts` is not reliable with different font-display loading patterns as any of them can fall back.

As noted, this does not mock responses from Google for the font resources themselves, which are currently loaded by the test browser. This means that we'll be dependent on this external service for passing integration tests until we implement caching of `url()`s in Turbopack. We should monitor the reliability of this test.
This commit is contained in:
Will Binns-Smith 2023-01-19 14:11:28 -08:00 committed by GitHub
parent 1d3e782841
commit 0aaf4fd87d
6 changed files with 184 additions and 6 deletions

View file

@ -3,6 +3,7 @@ use indexmap::IndexMap;
use indoc::formatdoc;
use once_cell::sync::Lazy;
use turbo_tasks::primitives::{OptionStringVc, OptionU16Vc, StringVc, U32Vc};
use turbo_tasks_env::{CommandLineProcessEnvVc, ProcessEnv};
use turbo_tasks_fetch::fetch;
use turbo_tasks_fs::{FileContent, FileSystemPathVc};
use turbo_tasks_hash::hash_xxh3_hash64;
@ -276,10 +277,19 @@ async fn get_request_hash(query_vc: QueryMapVc) -> Result<U32Vc> {
#[turbo_tasks::function]
async fn get_stylesheet_url_from_options(options: NextFontGoogleOptionsVc) -> Result<StringVc> {
let options = options.await?;
#[allow(unused_mut, unused_assignments)] // This is used in test environments
let mut css_url: Option<String> = None;
#[cfg(debug_assertions)]
{
let env = CommandLineProcessEnvVc::new();
if let Some(url) = &*env.read("TURBOPACK_TEST_ONLY_MOCK_SERVER").await? {
css_url = Some(format!("{}/css2", url));
}
}
let options = options.await?;
Ok(StringVc::cell(get_stylesheet_url(
GOOGLE_FONTS_STYLESHEET_URL,
css_url.as_deref().unwrap_or(GOOGLE_FONTS_STYLESHEET_URL),
&options.font_family,
&get_font_axes(
&FONT_DATA,

View file

@ -4,7 +4,10 @@ use turbopack_ecmascript::NextJsPageExportFilter;
use crate::{
next_server::context::ServerContextType,
next_shared::transforms::{get_next_dynamic_transform_rule, get_next_pages_transforms_rule},
next_shared::transforms::{
get_next_dynamic_transform_rule, get_next_font_transform_rule,
get_next_pages_transforms_rule,
},
};
/// Returns a list of module rules which apply server-side, Next.js-specific
@ -12,7 +15,7 @@ use crate::{
pub async fn get_next_server_transforms_rules(
context_ty: ServerContextType,
) -> Result<Vec<ModuleRule>> {
let mut rules = vec![];
let mut rules = vec![get_next_font_transform_rule()];
let (is_server_components, pages_dir) = match context_ty {
ServerContextType::Pages { pages_dir } => (false, Some(pages_dir)),

View file

@ -64,6 +64,7 @@ chromiumoxide = { version = "0.4.0", features = [
], default-features = false }
criterion = { version = "0.4.0", features = ["async_tokio"] }
fs_extra = "1.2.0"
httpmock = { version = "0.6.6", features = ["standalone"] }
lazy_static = "1.4.0"
once_cell = "1.13.0"
parking_lot = "0.12.1"

View file

@ -131,6 +131,9 @@ async fn run_test(resource: &str) -> JestRunResult {
let project_dir = workspace_root.join(resource);
let requested_addr = get_free_local_addr().unwrap();
let mock_dir = path.join("__httpmock__");
let mock_server_future = get_mock_server_future(&mock_dir);
let server = NextDevServerBuilder::new(
TurboTasks::new(MemoryBackend::new()),
sys_to_unix(&project_dir.to_string_lossy()).to_string(),
@ -159,10 +162,16 @@ async fn run_test(resource: &str) -> JestRunResult {
address = server.addr
);
tokio::select! {
let result = tokio::select! {
// Poll the mock_server first to add the env var
_ = mock_server_future => panic!("Never resolves"),
r = run_browser(server.addr) => r.expect("error while running browser"),
_ = server.future => panic!("Never resolves"),
}
};
env::remove_var("TURBOPACK_TEST_ONLY_MOCK_SERVER");
result
}
async fn create_browser(is_debugging: bool) -> Result<(Browser, JoinHandle<()>)> {
@ -248,3 +257,24 @@ fn get_free_local_addr() -> Result<SocketAddr, std::io::Error> {
socket.bind("127.0.0.1:0".parse().unwrap())?;
socket.local_addr()
}
async fn get_mock_server_future(mock_dir: &Path) -> Result<(), String> {
if mock_dir.exists() {
let port = get_free_local_addr().unwrap().port();
env::set_var(
"TURBOPACK_TEST_ONLY_MOCK_SERVER",
format!("http://127.0.0.1:{}", port),
);
httpmock::standalone::start_standalone_server(
port,
false,
Some(mock_dir.to_path_buf()),
false,
0,
)
.await
} else {
std::future::pending::<Result<(), String>>().await
}
}

View file

@ -0,0 +1,74 @@
when:
method: GET
path: /css2
query_param:
- name: family
value: "Inter:ital,wght@0,100..900"
- name: display
value: optional
then:
status: 200
body: |-
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: optional;
src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: optional;
src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: optional;
src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: optional;
src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: optional;
src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: optional;
src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: optional;
src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View file

@ -0,0 +1,60 @@
import { useEffect } from "react";
import { Inter } from "@next/font/google";
import { Deferred } from "@turbo/pack-test-harness/deferred";
const interNoArgs = Inter();
let testResult = new Deferred();
export default function Home() {
useEffect(() => {
// Only run on client
import("@turbo/pack-test-harness").then(runTests);
});
return <div className={interNoArgs.className}>Test</div>;
}
globalThis.waitForTests = function () {
return testResult.promise;
};
function runTests() {
it("returns structured data about the font styles from the font function", () => {
expect(interNoArgs).toEqual({
className:
"className◽[project-with-next]/crates/next-dev/tests/integration/next/font-google/basic/[embedded_modules]/@vercel/turbopack-next/internal/font/google/inter_34ab8b4d.module.css",
style: {
fontFamily: "'__Inter_34ab8b4d'",
fontStyle: "normal",
},
});
});
it("includes a rule styling the exported className", () => {
const selector = `.${CSS.escape(interNoArgs.className)}`;
let matchingRule;
for (const stylesheet of document.querySelectorAll(
"link[rel=stylesheet]"
)) {
const sheet = stylesheet.sheet;
if (sheet == null) {
continue;
}
for (const rule of sheet.cssRules) {
if (rule.selectorText === selector) {
matchingRule = rule;
break;
}
}
}
expect(matchingRule).toBeTruthy();
expect(matchingRule.style.fontFamily).toEqual("__Inter_34ab8b4d");
expect(matchingRule.style.fontStyle).toEqual("normal");
});
testResult.resolve(__jest__.run());
}