๐Ÿ“ฆ cloudflare / vinext

๐Ÿ“„ app-rendering.test.ts ยท 179 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179/**
 * Next.js Compatibility Tests: app-rendering
 *
 * Ported from: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-rendering/rendering.test.ts
 *
 * Tests fundamental rendering strategies in the App Router:
 * - SSR-only (revalidate = 0) with async data in layouts and pages
 * - Static (revalidate = false) with async data
 * - Parallel data fetching (layout + page data resolve concurrently)
 * - ISR (revalidate = 1) with Date.now() to verify revalidation
 *
 * Fixture pages live in: fixtures/app-basic/app/nextjs-compat/
 */

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import type { ViteDevServer } from "vite";
import { APP_FIXTURE_DIR, startFixtureServer, fetchHtml } from "../helpers.js";

describe("Next.js compat: app-rendering", () => {
  let server: ViteDevServer;
  let baseUrl: string;

  beforeAll(async () => {
    ({ server, baseUrl } = await startFixtureServer(APP_FIXTURE_DIR, {
      appRouter: true,
    }));
    // Warm up the server โ€” first request after startup can be slow
    // as Vite compiles the RSC entry, SSR entry, and client entry.
    await fetch(`${baseUrl}/`).catch(() => {});
  }, 60_000);

  afterAll(async () => {
    await server?.close();
  });

  // โ”€โ”€ Root page โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // Next.js: it('should serve app/page.server.js at /', ...)
  // We use a sub-route /nextjs-compat since we share the app-basic fixture.
  it("should serve the nextjs-compat root page", async () => {
    const { html } = await fetchHtml(baseUrl, "/nextjs-compat");
    expect(html).toContain("app/page.server.js");
  });

  // โ”€โ”€ SSR only โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  describe("SSR only (revalidate = 0)", () => {
    // Next.js: it('should run data in layout and page', ...)
    // Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-rendering/rendering.test.ts#L18-L22
    it("should run async data in layout and page", async () => {
      const { html } = await fetchHtml(
        baseUrl,
        "/nextjs-compat/ssr-only/nested",
      );
      // Layout renders: <h1 id="layout-message">hello from layout</h1>
      expect(html).toContain("hello from layout");
      // Page renders: <p id="page-message">hello from page</p>
      expect(html).toContain("hello from page");
    });

    // Next.js: it('should run data fetch in parallel', ...)
    // Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-rendering/rendering.test.ts#L24-L33
    // NOTE: Delays reduced from 5s to 1s. Threshold adjusted from 10s to 3s.
    it("should run data fetch in parallel (layout + page concurrent)", async () => {
      const startTime = Date.now();
      const { html } = await fetchHtml(
        baseUrl,
        "/nextjs-compat/ssr-only/slow",
      );
      const duration = Date.now() - startTime;

      // Each part takes 1s. If sequential it would take 2s+.
      // If parallel it should complete in ~1s. Use 3s threshold for CI safety.
      expect(duration).toBeLessThan(3_000);
      expect(html).toContain("hello from slow layout");
      expect(html).toContain("hello from slow page");
    });
  });

  // โ”€โ”€ Static only โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  describe("static only (revalidate = false)", () => {
    // Next.js: it('should run data in layout and page', ...)
    // Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-rendering/rendering.test.ts#L37-L41
    it("should run async data in layout and page", async () => {
      const { html } = await fetchHtml(
        baseUrl,
        "/nextjs-compat/static-only/nested",
      );
      expect(html).toContain("hello from layout");
      expect(html).toContain("hello from page");
    });

    // Next.js: it('should run data in parallel ... during development', ...)
    // Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-rendering/rendering.test.ts#L43-L55
    // NOTE: Delays reduced from 5s to 1s.
    it("should run data in parallel during development", async () => {
      const { html } = await fetchHtml(
        baseUrl,
        "/nextjs-compat/static-only/slow",
      );
      expect(html).toContain("hello from slow layout");
      expect(html).toContain("hello from slow page");
    });
  });

  // โ”€โ”€ ISR โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  describe("ISR (revalidate = 1)", () => {
    // Next.js: it('should revalidate the page when revalidate is configured', ...)
    // Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-rendering/rendering.test.ts#L59-L86
    it("should render ISR page with layout and page data", async () => {
      const { html } = await fetchHtml(
        baseUrl,
        "/nextjs-compat/isr-multiple/nested",
      );
      expect(html).toContain("hello from layout");
      expect(html).toContain("hello from page");
      // Should contain timestamp values from Date.now()
      expect(html).toMatch(/id="layout-now"[^>]*>\d+/);
      expect(html).toMatch(/id="page-now"[^>]*>\d+/);
    });

    // This tests that subsequent requests get fresh timestamps (revalidation works).
    // In dev mode, vinext always re-renders (no ISR caching), so timestamps should differ.
    // SKIP: The use(getData()) pattern with Date.now() in the ISR layout produces identical
    // timestamps across requests. The async function getData() returns a cached promise at
    // module scope in the RSC environment, so Date.now() is evaluated once.
    //
    // ROOT CAUSE: vinext's RSC module instances persist across requests in dev mode.
    // Next.js re-executes server components fresh per request by invalidating the module cache.
    // Note: The ISR cache has been removed from dev mode (issue #228), but this test still
    // fails because the underlying module caching issue is separate from ISR.
    //
    // TO FIX: The RSC environment needs to invalidate/re-import server component modules on
    // each request so that top-level expressions like Date.now() get re-evaluated. This may
    // involve calling server.moduleGraph.invalidateModule() for RSC modules before each render,
    // or using Vite's ssrLoadModule with a cache-bust query param.
    //
    // VERIFY: Once fixed, also confirm that the "Invalid hook call" warnings from use() go away
    // (they may be related to the same module caching causing duplicate React instances).
    it.skip("should produce different timestamps on subsequent requests", async () => {
      const { html: html1 } = await fetchHtml(
        baseUrl,
        "/nextjs-compat/isr-multiple/nested",
      );
      const layoutNow1 = html1.match(/id="layout-now"[^>]*>(\d+)/)?.[1];
      const pageNow1 = html1.match(/id="page-now"[^>]*>(\d+)/)?.[1];

      // Wait for revalidation window (revalidate = 1 second)
      await new Promise((r) => setTimeout(r, 1500));

      const { html: html2 } = await fetchHtml(
        baseUrl,
        "/nextjs-compat/isr-multiple/nested",
      );
      const layoutNow2 = html2.match(/id="layout-now"[^>]*>(\d+)/)?.[1];
      const pageNow2 = html2.match(/id="page-now"[^>]*>(\d+)/)?.[1];

      expect(layoutNow1).toBeTruthy();
      expect(pageNow1).toBeTruthy();
      expect(layoutNow2).toBeTruthy();
      expect(pageNow2).toBeTruthy();

      // In dev mode, timestamps should always differ (fresh render each time)
      expect(layoutNow1).not.toBe(layoutNow2);
      expect(pageNow1).not.toBe(pageNow2);
    });
  });

  // โ”€โ”€ Mixed static and dynamic (skipped in Next.js too) โ”€โ”€โ”€โ”€โ”€โ”€
  // Next.js: describe.skip('mixed static and dynamic', ...)
  // Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-rendering/rendering.test.ts#L89-L113
  describe.skip("mixed static and dynamic", () => {
    // SKIP: This is also skipped in the Next.js test suite.
    // It tests a scenario where layout uses getServerSideProps-like behavior
    // and page uses getStaticProps-like behavior. Not yet implemented in either project.
    it("should generate static data during build and use it", () => {
      // Placeholder for future implementation
    });
  });
});