create-ailk 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +11 -9
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +294 -12
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +9 -32
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +0 -4
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +3 -2
- package/dist/lib/scaffold-render-sweep.js +5 -8
- package/dist/module-architecture.js +98 -10
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/spoke-checkout.d.ts +38 -35
- package/dist/spoke-checkout.js +66 -229
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +33 -12
- package/dist/sync-routes.d.ts +38 -1
- package/dist/sync-routes.js +184 -11
- package/dist/template-record.d.ts +1 -1
- package/dist/template-record.js +3 -2
- package/dist/update.js +10 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +9 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +27 -0
- package/templates/apps/api/CLAUDE.md +77 -16
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/stripe.ts +74 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/spec.ts +10 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +4 -2
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +17 -14
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +91 -17
- package/templates/apps/api/src/server.ts +140 -51
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
- package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +113 -58
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +124 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +22 -14
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +315 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +6 -2
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +9 -20
- package/templates/apps/web/lib/deck-page.ts +1 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +42 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +202 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
- package/templates/apps/web/messages/en.json +49 -15
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +31 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +31 -1
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +30 -2
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
- package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/checkout.ts +0 -93
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The careers set end to end (#8849 D7, D8, D9, D10; AC-7): one `writer`
|
|
3
|
+
* posting, served by one shared content mock, through /careers, the posting
|
|
4
|
+
* page and the Free apply page. The `signal-collectors` walk over the same
|
|
5
|
+
* posting is apply-routes.test.tsx's (#8840 D11), which reads the moved
|
|
6
|
+
* `lib/posting-title.ts` through the same page-renderer loader.
|
|
7
|
+
*/
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { render, screen } from "@testing-library/react";
|
|
10
|
+
import * as pageRenderer from "@working-theory/page-renderer";
|
|
11
|
+
import { jobApplicationFlowConfig } from "@working-theory/validation";
|
|
12
|
+
|
|
13
|
+
jest.mock("next/navigation", () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
notFound: jest.fn(() => {
|
|
16
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
MDXRemote: ({ source }: { source: string }) =>
|
|
23
|
+
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
27
|
+
flows: { get: jest.fn() },
|
|
28
|
+
leads: { captureStatus: jest.fn(), submit: jest.fn() },
|
|
29
|
+
visitorForwardHeaders: () => undefined,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.mock("next/headers", () => ({
|
|
33
|
+
__esModule: true,
|
|
34
|
+
headers: jest.fn().mockResolvedValue({ get: () => null }),
|
|
35
|
+
cookies: jest.fn().mockResolvedValue({ get: () => undefined }),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock: the real read is a node:fs frontmatter parse of content/_site.mdx; narrowest possible scope
|
|
39
|
+
jest.mock("../../contact/site-contact-email", () => ({
|
|
40
|
+
readSiteContactEmail: jest.fn().mockReturnValue("jobs@example.com"),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
import { flows, leads } from "@working-theory/api-client";
|
|
44
|
+
|
|
45
|
+
import CareersPage from "../page";
|
|
46
|
+
import JobPage from "../[slug]/page";
|
|
47
|
+
import ApplyPage from "../[slug]/[action]/page";
|
|
48
|
+
|
|
49
|
+
const WRITER = {
|
|
50
|
+
type: "JobPosting" as const,
|
|
51
|
+
slug: "writer",
|
|
52
|
+
title: "Writer",
|
|
53
|
+
description: "Write for us.",
|
|
54
|
+
employmentType: "FULL_TIME" as const,
|
|
55
|
+
remote: true,
|
|
56
|
+
location: "Remote (US)",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type GetMdxSource = typeof pageRenderer.getMdxSource;
|
|
60
|
+
|
|
61
|
+
const getMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>(async (type, slug) => {
|
|
62
|
+
if (type === "CareersList")
|
|
63
|
+
return {
|
|
64
|
+
frontmatter: {
|
|
65
|
+
type: "CareersList" as const,
|
|
66
|
+
slug: "careers",
|
|
67
|
+
title: "Careers",
|
|
68
|
+
description: "Open roles.",
|
|
69
|
+
},
|
|
70
|
+
body: "",
|
|
71
|
+
};
|
|
72
|
+
if (type === "JobPosting" && slug === "writer")
|
|
73
|
+
return { frontmatter: WRITER, body: "The writer role." };
|
|
74
|
+
return null;
|
|
75
|
+
});
|
|
76
|
+
const getMdxSlugs = jest.fn().mockResolvedValue(["writer"]);
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
// `lib/posting-title.ts` reads through the package loader, not a prop.
|
|
80
|
+
jest.spyOn(pageRenderer, "getMdxSource").mockImplementation(getMdxSource);
|
|
81
|
+
(flows.get as jest.Mock).mockResolvedValue({
|
|
82
|
+
ok: true,
|
|
83
|
+
status: 200,
|
|
84
|
+
body: jobApplicationFlowConfig,
|
|
85
|
+
});
|
|
86
|
+
(leads.captureStatus as jest.Mock).mockResolvedValue({
|
|
87
|
+
ok: true,
|
|
88
|
+
status: 200,
|
|
89
|
+
body: { ok: true, data: { contact: true, newsletter: true } },
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
jest.restoreAllMocks();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("a writer posting renders through the careers set (#8849 AC-7)", () => {
|
|
98
|
+
it("/careers' generated card links to /careers/writer", async () => {
|
|
99
|
+
render(
|
|
100
|
+
await CareersPage({
|
|
101
|
+
params: Promise.resolve({ locale: "en" }),
|
|
102
|
+
getMdxSource,
|
|
103
|
+
getMdxSlugs,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
const links = Array.from(document.querySelectorAll("a")).map((a) =>
|
|
107
|
+
a.getAttribute("href"),
|
|
108
|
+
);
|
|
109
|
+
expect(links).toContain("/careers/writer");
|
|
110
|
+
expect(screen.getByTestId("items-filter-search")).toBeTruthy();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("/careers/writer renders the posting, its Apply button, and an ApplyAction to the same page", async () => {
|
|
114
|
+
const { container } = render(
|
|
115
|
+
await JobPage({
|
|
116
|
+
params: Promise.resolve({ locale: "en", slug: "writer" }),
|
|
117
|
+
getMdxSource,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
expect(screen.getByRole("heading", { level: 1, name: "Writer" })).toBeTruthy();
|
|
121
|
+
const cta = screen.getByTestId("job-apply-cta");
|
|
122
|
+
expect(cta.getAttribute("href")).toBe("/careers/writer/apply");
|
|
123
|
+
|
|
124
|
+
const jsonLd = JSON.parse(
|
|
125
|
+
container.querySelector("[data-testid='json-ld']")!.textContent!,
|
|
126
|
+
) as { "@graph": { "@type": string; mainEntity?: Record<string, unknown> }[] };
|
|
127
|
+
const posting = jsonLd["@graph"].find((node) => node["@type"] === "WebPage")!
|
|
128
|
+
.mainEntity!;
|
|
129
|
+
expect(posting["potentialAction"]).toEqual({
|
|
130
|
+
"@type": "ApplyAction",
|
|
131
|
+
target: "http://localhost:3000/en/careers/writer/apply",
|
|
132
|
+
});
|
|
133
|
+
expect(posting["employmentType"]).toBe("FULL_TIME");
|
|
134
|
+
expect(posting["jobLocationType"]).toBe("TELECOMMUTE");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("the Free page at /careers/writer/apply renders the form bound to writer", async () => {
|
|
138
|
+
render(
|
|
139
|
+
await ApplyPage({
|
|
140
|
+
params: Promise.resolve({ locale: "en", slug: "writer", action: "apply" }),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
expect(screen.getByRole("heading", { level: 1, name: "Writer" })).toBeTruthy();
|
|
144
|
+
expect(screen.getByTestId("lead-capture-form")).toBeTruthy();
|
|
145
|
+
expect(screen.getByTestId("nav-back-link").getAttribute("href")).toBe(
|
|
146
|
+
"/careers/writer",
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* form that stands in for an absent or switched-off magnet.
|
|
5
5
|
*/
|
|
6
6
|
import {
|
|
7
|
+
JOB_APPLICATION_TERMS_STATEMENT,
|
|
7
8
|
jobApplicationFlowConfig,
|
|
8
9
|
type FlowConfig,
|
|
9
10
|
} from "@working-theory/validation";
|
|
@@ -18,7 +19,10 @@ const clone = (): FlowConfig =>
|
|
|
18
19
|
JSON.parse(JSON.stringify(jobApplicationFlowConfig)) as FlowConfig;
|
|
19
20
|
|
|
20
21
|
describe("jobApplicationFormFromConfig (#8746 D5, AC-4)", () => {
|
|
21
|
-
|
|
22
|
+
// #8840 D3 — the seed is two steps: step 1 (names, email, phone, the hidden
|
|
23
|
+
// `job`, the terms box) draws inline; LinkedIn URL moved to step 2, whose
|
|
24
|
+
// "Submit application" is now the submit label.
|
|
25
|
+
it("maps the seeded config's step 1 to First, Last, Email and Phone, with the step's title and step 2's submit label", () => {
|
|
22
26
|
const view = jobApplicationFormFromConfig(jobApplicationFlowConfig, "en");
|
|
23
27
|
expect(view?.title).toBe("Job application");
|
|
24
28
|
expect(view?.fields).toEqual([
|
|
@@ -50,13 +54,6 @@ describe("jobApplicationFormFromConfig (#8746 D5, AC-4)", () => {
|
|
|
50
54
|
required: false,
|
|
51
55
|
colSpan: 1,
|
|
52
56
|
},
|
|
53
|
-
{
|
|
54
|
-
name: "linkedin_url",
|
|
55
|
-
type: "url",
|
|
56
|
-
label: "LinkedIn URL",
|
|
57
|
-
required: false,
|
|
58
|
-
colSpan: 2,
|
|
59
|
-
},
|
|
60
57
|
]);
|
|
61
58
|
expect(view?.submitLabel).toBe("Submit application");
|
|
62
59
|
});
|
|
@@ -67,7 +64,7 @@ describe("jobApplicationFormFromConfig (#8746 D5, AC-4)", () => {
|
|
|
67
64
|
FlowConfig["steps"][number],
|
|
68
65
|
{ type: "field" }
|
|
69
66
|
>;
|
|
70
|
-
step.fields[
|
|
67
|
+
step.fields[3]!.copy = { label: { en: "Mobile" } };
|
|
71
68
|
step.fields.push({
|
|
72
69
|
name: "cover_letter",
|
|
73
70
|
inputType: "textarea",
|
|
@@ -78,8 +75,7 @@ describe("jobApplicationFormFromConfig (#8746 D5, AC-4)", () => {
|
|
|
78
75
|
["first_name", "First name", 1],
|
|
79
76
|
["last_name", "Last name", 1],
|
|
80
77
|
["email", "Email", 1],
|
|
81
|
-
["phone", "
|
|
82
|
-
["linkedin_url", "Portfolio", 2],
|
|
78
|
+
["phone", "Mobile", 1],
|
|
83
79
|
["cover_letter", "Cover letter", 2],
|
|
84
80
|
]);
|
|
85
81
|
});
|
|
@@ -111,26 +107,28 @@ describe("a consent field on the magnet (#8746 D5)", () => {
|
|
|
111
107
|
FlowConfig["steps"][number],
|
|
112
108
|
{ type: "field" }
|
|
113
109
|
>;
|
|
114
|
-
step.fields.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
purposes: ["service_terms"],
|
|
119
|
-
copy: {
|
|
120
|
-
label: {
|
|
121
|
-
en: "I agree my application is kept for 12 months under the [Privacy Policy](privacy).",
|
|
122
|
-
},
|
|
110
|
+
const terms = step.fields.find((f) => f.name === "terms")!;
|
|
111
|
+
terms.copy = {
|
|
112
|
+
label: {
|
|
113
|
+
en: "I agree my application is kept for 12 months under the [Privacy Policy](privacy).",
|
|
123
114
|
},
|
|
124
|
-
}
|
|
115
|
+
};
|
|
125
116
|
const view = jobApplicationFormFromConfig(config, "en");
|
|
126
117
|
expect(view?.consentStatement).toBe(
|
|
127
118
|
"I agree my application is kept for 12 months under the [Privacy Policy](privacy).",
|
|
128
119
|
);
|
|
129
|
-
expect(view?.fields.map((f) => f.name)).not.toContain("
|
|
120
|
+
expect(view?.fields.map((f) => f.name)).not.toContain("terms");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// #8840 D3 — the seed now carries a terms box, so the page labels its own.
|
|
124
|
+
it("the seeded config's terms field labels the box", () => {
|
|
125
|
+
expect(jobApplicationFormFallback("en").consentStatement).toBe(
|
|
126
|
+
JOB_APPLICATION_TERMS_STATEMENT,
|
|
127
|
+
);
|
|
130
128
|
});
|
|
131
129
|
|
|
132
|
-
it("
|
|
133
|
-
expect(jobApplicationFormFallback("en").
|
|
130
|
+
it("never draws the hidden job field", () => {
|
|
131
|
+
expect(jobApplicationFormFallback("en").fields.map((f) => f.name)).not.toContain("job");
|
|
134
132
|
});
|
|
135
133
|
});
|
|
136
134
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* careers
|
|
3
|
-
*
|
|
2
|
+
* The careers set over the Job application magnet (#8746 D1, D5; #8849 D7,
|
|
3
|
+
* D8; AC-5, AC-6): the posting page carries an Apply button and no form, and
|
|
4
|
+
* the Free apply page, careers/[slug]/[action]/page.tsx, draws the form.
|
|
4
5
|
*
|
|
5
6
|
* Renders the REAL `LeadCaptureForm` bound to the REAL apply action, so the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `leads.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* form, its fields and a submit are all visible. Mocked boundaries:
|
|
8
|
+
* `@working-theory/api-client` (`flows.get`, `leads.captureStatus`,
|
|
9
|
+
* `leads.submit`), `next/headers`, `next-intl/server` (over the real English
|
|
10
|
+
* catalog), the `_site.mdx` contact-email read, and the page-renderer's
|
|
11
|
+
* content loader (injected, and spied on for `lib/posting-title.ts`'s read).
|
|
11
12
|
*/
|
|
12
13
|
import React from "react";
|
|
13
14
|
import {
|
|
@@ -69,34 +70,40 @@ jest.mock("../../contact/site-contact-email", () => ({
|
|
|
69
70
|
readSiteContactEmail: jest.fn().mockReturnValue("jobs@example.com"),
|
|
70
71
|
}));
|
|
71
72
|
|
|
73
|
+
import { readFileSync } from "node:fs";
|
|
74
|
+
import { join } from "node:path";
|
|
75
|
+
|
|
72
76
|
import { flows, leads } from "@working-theory/api-client";
|
|
77
|
+
import * as pageRenderer from "@working-theory/page-renderer";
|
|
73
78
|
|
|
74
79
|
import messages from "../../../../messages/en.json";
|
|
75
80
|
|
|
76
81
|
import CareersPage from "../page";
|
|
77
82
|
import JobPage from "../[slug]/page";
|
|
83
|
+
import * as jobPageModule from "../[slug]/page";
|
|
84
|
+
import ApplyPage, { generateMetadata as applyMetadata } from "../[slug]/[action]/page";
|
|
78
85
|
|
|
79
86
|
const mockFlowsGet = flows.get as jest.Mock;
|
|
80
87
|
const mockCaptureStatus = leads.captureStatus as jest.Mock;
|
|
81
88
|
const mockSubmit = leads.submit as jest.Mock;
|
|
89
|
+
let mockPostingSource: jest.SpyInstance;
|
|
82
90
|
|
|
83
91
|
type FieldStep = Extract<FlowConfig["steps"][number], { type: "field" }>;
|
|
84
92
|
|
|
85
|
-
const JOB = "
|
|
93
|
+
const JOB = "writer";
|
|
86
94
|
const jobSource = {
|
|
87
95
|
frontmatter: {
|
|
88
96
|
type: "JobPosting" as const,
|
|
89
97
|
slug: JOB,
|
|
90
|
-
title: "
|
|
91
|
-
description: "
|
|
98
|
+
title: "Writer",
|
|
99
|
+
description: "Write for us.",
|
|
92
100
|
},
|
|
93
101
|
body: "mdx-body-content",
|
|
94
102
|
};
|
|
95
103
|
|
|
96
|
-
async function
|
|
97
|
-
const element = await
|
|
98
|
-
params: Promise.resolve({ locale: "en", slug
|
|
99
|
-
getMdxSource: jest.fn().mockResolvedValue(jobSource),
|
|
104
|
+
async function renderApplyPage(slug = JOB, action = "apply") {
|
|
105
|
+
const element = await ApplyPage({
|
|
106
|
+
params: Promise.resolve({ locale: "en", slug, action }),
|
|
100
107
|
});
|
|
101
108
|
return render(element);
|
|
102
109
|
}
|
|
@@ -112,9 +119,9 @@ function fieldNames(): string[] {
|
|
|
112
119
|
.filter((name) => !name.includes("-"));
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
// The
|
|
116
|
-
// (as /contact does, #8731). Each test starts an hour later so none
|
|
117
|
-
// another's read.
|
|
122
|
+
// The apply page shares its two hub reads for a few seconds, per module
|
|
123
|
+
// instance (as /contact does, #8731). Each test starts an hour later so none
|
|
124
|
+
// sees another's read.
|
|
118
125
|
let clock = 1_800_000_000_000;
|
|
119
126
|
const nowSpy = jest.spyOn(Date, "now");
|
|
120
127
|
|
|
@@ -132,50 +139,86 @@ beforeEach(() => {
|
|
|
132
139
|
body: jobApplicationFlowConfig,
|
|
133
140
|
});
|
|
134
141
|
mockSubmit.mockResolvedValue({ ok: true, status: 201, body: { ok: true } });
|
|
142
|
+
// `lib/posting-title.ts` reads the posting through the package loader.
|
|
143
|
+
mockPostingSource = jest
|
|
144
|
+
.spyOn(pageRenderer, "getMdxSource")
|
|
145
|
+
.mockImplementation(async (_type, slug) => (slug === JOB ? jobSource : null));
|
|
135
146
|
});
|
|
136
147
|
|
|
137
148
|
afterEach(() => {
|
|
138
149
|
jest.clearAllMocks();
|
|
150
|
+
mockPostingSource.mockRestore();
|
|
139
151
|
});
|
|
140
152
|
|
|
141
|
-
describe("
|
|
142
|
-
it("
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
describe("the posting page carries an Apply button and no form (#8849 D8, AC-5)", () => {
|
|
154
|
+
it("renders job-apply-cta to the localized apply page, and no form or hub read", async () => {
|
|
155
|
+
const element = await JobPage({
|
|
156
|
+
params: Promise.resolve({ locale: "fr", slug: JOB }),
|
|
157
|
+
getMdxSource: jest.fn().mockResolvedValue(jobSource),
|
|
158
|
+
});
|
|
159
|
+
render(element);
|
|
160
|
+
const cta = screen.getByTestId("job-apply-cta");
|
|
161
|
+
expect(cta.getAttribute("href")).toBe("/fr/careers/writer/apply");
|
|
162
|
+
expect(cta.textContent).toBe(messages.careers.apply);
|
|
163
|
+
expect(screen.queryByTestId("job-apply")).toBeNull();
|
|
164
|
+
expect(document.getElementById("apply")).toBeNull();
|
|
165
|
+
expect(screen.queryByTestId("lead-capture-form")).toBeNull();
|
|
166
|
+
expect(document.querySelector("form")).toBeNull();
|
|
167
|
+
expect(mockFlowsGet).not.toHaveBeenCalled();
|
|
168
|
+
expect(mockCaptureStatus).not.toHaveBeenCalled();
|
|
169
|
+
// The posting itself still renders above it.
|
|
170
|
+
expect(screen.getByTestId("mdx-rendered").textContent).toBe(
|
|
171
|
+
"mdx-body-content",
|
|
149
172
|
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("an unknown posting is a 404 and builds no button", async () => {
|
|
176
|
+
await expect(
|
|
177
|
+
JobPage({
|
|
178
|
+
params: Promise.resolve({ locale: "en", slug: "unknown" }),
|
|
179
|
+
getMdxSource: jest.fn().mockResolvedValue(null),
|
|
180
|
+
}),
|
|
181
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
182
|
+
expect(screen.queryByTestId("job-apply-cta")).toBeNull();
|
|
183
|
+
expect(mockFlowsGet).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("the page module no longer exports dynamic", () => {
|
|
187
|
+
expect("dynamic" in jobPageModule).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("the Free apply page draws the Job application (#8849 D7, AC-6; #8746 D1, D5)", () => {
|
|
192
|
+
it("renders a back link to the posting, the posting's title and the magnet's field step", async () => {
|
|
193
|
+
await renderApplyPage();
|
|
194
|
+
expect(mockFlowsGet).toHaveBeenCalledWith("job-application");
|
|
150
195
|
expect(
|
|
151
|
-
screen.getByRole("heading", { level:
|
|
196
|
+
screen.getByRole("heading", { level: 1, name: "Writer" }),
|
|
152
197
|
).toBeTruthy();
|
|
198
|
+
expect(screen.getByTestId("nav-back-link").getAttribute("href")).toBe(
|
|
199
|
+
"/careers/writer",
|
|
200
|
+
);
|
|
153
201
|
expect(fieldNames()).toEqual([
|
|
154
202
|
"first_name",
|
|
155
203
|
"last_name",
|
|
156
204
|
"email",
|
|
157
205
|
"phone",
|
|
158
|
-
"linkedin_url",
|
|
159
206
|
]);
|
|
160
207
|
expect(screen.getByTestId("contact-submit").textContent).toBe(
|
|
161
208
|
"Submit application",
|
|
162
209
|
);
|
|
163
|
-
// The posting itself still renders above it.
|
|
164
|
-
expect(screen.getByTestId("mdx-rendered").textContent).toBe(
|
|
165
|
-
"mdx-body-content",
|
|
166
|
-
);
|
|
167
210
|
});
|
|
168
211
|
|
|
169
212
|
it("an owner's edit to the magnet shows on the page", async () => {
|
|
170
213
|
const edited = JSON.parse(
|
|
171
214
|
JSON.stringify(jobApplicationFlowConfig),
|
|
172
215
|
) as FlowConfig;
|
|
173
|
-
(edited.steps[0] as FieldStep).fields[
|
|
216
|
+
(edited.steps[0] as FieldStep).fields[2]!.copy.label = { en: "Work email" };
|
|
174
217
|
mockFlowsGet.mockResolvedValue({ ok: true, status: 200, body: edited });
|
|
175
|
-
await
|
|
218
|
+
await renderApplyPage();
|
|
176
219
|
expect(
|
|
177
|
-
screen.getByTestId("field-
|
|
178
|
-
).toContain("
|
|
220
|
+
screen.getByTestId("field-email").closest("div")!.textContent,
|
|
221
|
+
).toContain("Work email");
|
|
179
222
|
});
|
|
180
223
|
|
|
181
224
|
it.each([
|
|
@@ -203,14 +246,12 @@ describe("a job page renders the Job application under #apply (#8746 D1, D5, AC-
|
|
|
203
246
|
"falls back to the built-in form with the same fields when the magnet is %s",
|
|
204
247
|
async (_label, arrange) => {
|
|
205
248
|
arrange();
|
|
206
|
-
await
|
|
207
|
-
expect(document.getElementById("apply")).not.toBeNull();
|
|
249
|
+
await renderApplyPage();
|
|
208
250
|
expect(fieldNames()).toEqual([
|
|
209
251
|
"first_name",
|
|
210
252
|
"last_name",
|
|
211
253
|
"email",
|
|
212
254
|
"phone",
|
|
213
|
-
"linkedin_url",
|
|
214
255
|
]);
|
|
215
256
|
},
|
|
216
257
|
);
|
|
@@ -221,30 +262,65 @@ describe("a job page renders the Job application under #apply (#8746 D1, D5, AC-
|
|
|
221
262
|
status: 200,
|
|
222
263
|
body: { ok: true, data: { contact: false, newsletter: false } },
|
|
223
264
|
});
|
|
224
|
-
await
|
|
225
|
-
expect(document.getElementById("apply")).not.toBeNull();
|
|
265
|
+
await renderApplyPage();
|
|
226
266
|
expect(screen.queryByTestId("lead-capture-form")).toBeNull();
|
|
227
|
-
expect(
|
|
267
|
+
expect(screen.getByTestId("job-apply").textContent).toContain(
|
|
228
268
|
"jobs@example.com",
|
|
229
269
|
);
|
|
230
270
|
});
|
|
231
271
|
|
|
232
|
-
it(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
).
|
|
272
|
+
it.each([
|
|
273
|
+
["an action other than apply", JOB, "start"],
|
|
274
|
+
["an uppercase slug", "Writer", "apply"],
|
|
275
|
+
["an encoded traversal slug", "a%2F..%2Fb", "apply"],
|
|
276
|
+
])("%s is a 404 before any read", async (_label, slug, action) => {
|
|
277
|
+
await expect(renderApplyPage(slug, action)).rejects.toThrow("NEXT_NOT_FOUND");
|
|
278
|
+
expect(mockPostingSource).not.toHaveBeenCalled();
|
|
279
|
+
expect(mockFlowsGet).not.toHaveBeenCalled();
|
|
280
|
+
expect(mockCaptureStatus).not.toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("an unknown posting is a 404 with no hub read", async () => {
|
|
284
|
+
await expect(renderApplyPage("unknown")).rejects.toThrow("NEXT_NOT_FOUND");
|
|
285
|
+
expect(mockPostingSource).toHaveBeenCalledWith("JobPosting", "unknown", "en");
|
|
239
286
|
expect(mockFlowsGet).not.toHaveBeenCalled();
|
|
287
|
+
expect(mockCaptureStatus).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("is noindex, follow, titled with the posting", async () => {
|
|
291
|
+
const metadata = await applyMetadata({
|
|
292
|
+
params: Promise.resolve({ locale: "en", slug: JOB, action: "apply" }),
|
|
293
|
+
});
|
|
294
|
+
expect(metadata.robots).toEqual({ index: false, follow: true });
|
|
295
|
+
expect(metadata.title).toBe("Writer");
|
|
296
|
+
const unknown = await applyMetadata({
|
|
297
|
+
params: Promise.resolve({ locale: "en", slug: "unknown", action: "apply" }),
|
|
298
|
+
});
|
|
299
|
+
expect(unknown.robots).toEqual({ index: false, follow: true });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("is core: it imports no flows/ or module file, and reads the title from lib/posting-title", () => {
|
|
303
|
+
const source = readFileSync(
|
|
304
|
+
join(__dirname, "..", "[slug]", "[action]", "page.tsx"),
|
|
305
|
+
"utf8",
|
|
306
|
+
);
|
|
307
|
+
const imports = source
|
|
308
|
+
.split("\n")
|
|
309
|
+
.filter((line) => /^import |^\} from |^ [\w{}, ]+ from /.test(line) || line.includes(" from \""))
|
|
310
|
+
.join("\n");
|
|
311
|
+
expect(imports).not.toMatch(/flows\//);
|
|
312
|
+
expect(imports).not.toContain("FlowStepper");
|
|
313
|
+
expect(imports).not.toContain("@working-theory/ui-signal-collectors");
|
|
314
|
+
expect(imports).not.toMatch(/["']\.\/apply|\/apply\//);
|
|
315
|
+
expect(imports).toContain('from "../../../../../lib/posting-title"');
|
|
240
316
|
});
|
|
241
317
|
});
|
|
242
318
|
|
|
243
|
-
describe("the
|
|
319
|
+
describe("the Free apply page shares its two hub reads (#8746 security review, as #8731)", () => {
|
|
244
320
|
it("a burst of views makes one hub read of each kind, not one per view", async () => {
|
|
245
321
|
for (let i = 0; i < 25; i += 1) {
|
|
246
|
-
const view = await
|
|
247
|
-
expect(
|
|
322
|
+
const view = await renderApplyPage();
|
|
323
|
+
expect(screen.getByTestId("lead-capture-form")).toBeTruthy();
|
|
248
324
|
view.unmount();
|
|
249
325
|
}
|
|
250
326
|
expect(mockFlowsGet).toHaveBeenCalledTimes(1);
|
|
@@ -255,13 +331,12 @@ describe("the job page shares its two hub reads (#8746 security review, as #8731
|
|
|
255
331
|
mockFlowsGet.mockRejectedValue(new Error("hub down"));
|
|
256
332
|
mockCaptureStatus.mockRejectedValue(new Error("hub down"));
|
|
257
333
|
for (let i = 0; i < 5; i += 1) {
|
|
258
|
-
const view = await
|
|
334
|
+
const view = await renderApplyPage();
|
|
259
335
|
expect(fieldNames()).toEqual([
|
|
260
336
|
"first_name",
|
|
261
337
|
"last_name",
|
|
262
338
|
"email",
|
|
263
339
|
"phone",
|
|
264
|
-
"linkedin_url",
|
|
265
340
|
]);
|
|
266
341
|
view.unmount();
|
|
267
342
|
}
|
|
@@ -270,34 +345,33 @@ describe("the job page shares its two hub reads (#8746 security review, as #8731
|
|
|
270
345
|
});
|
|
271
346
|
|
|
272
347
|
it("an Admin edit shows once the shared read expires", async () => {
|
|
273
|
-
const first = await
|
|
348
|
+
const first = await renderApplyPage();
|
|
274
349
|
expect(
|
|
275
|
-
screen.getByTestId("field-
|
|
276
|
-
).toContain("
|
|
350
|
+
screen.getByTestId("field-email").closest("div")!.textContent,
|
|
351
|
+
).toContain("Email");
|
|
277
352
|
first.unmount();
|
|
278
353
|
|
|
279
354
|
const edited = JSON.parse(
|
|
280
355
|
JSON.stringify(jobApplicationFlowConfig),
|
|
281
356
|
) as FlowConfig;
|
|
282
|
-
(edited.steps[0] as FieldStep).fields[
|
|
357
|
+
(edited.steps[0] as FieldStep).fields[2]!.copy.label = { en: "Work email" };
|
|
283
358
|
mockFlowsGet.mockResolvedValue({ ok: true, status: 200, body: edited });
|
|
284
359
|
clock += 60_000;
|
|
285
|
-
await
|
|
360
|
+
await renderApplyPage();
|
|
286
361
|
expect(
|
|
287
|
-
screen.getByTestId("field-
|
|
288
|
-
).toContain("
|
|
362
|
+
screen.getByTestId("field-email").closest("div")!.textContent,
|
|
363
|
+
).toContain("Work email");
|
|
289
364
|
expect(mockFlowsGet).toHaveBeenCalledTimes(2);
|
|
290
365
|
});
|
|
291
366
|
});
|
|
292
367
|
|
|
293
|
-
describe("a submission from the
|
|
368
|
+
describe("a submission from the Free apply page lands as a job application lead (#8746 D4, D5; #8849 AC-6)", () => {
|
|
294
369
|
it("posts source flow:job-application, name = first + last and the posting's slug as job", async () => {
|
|
295
|
-
await
|
|
370
|
+
await renderApplyPage();
|
|
296
371
|
for (const [name, value] of Object.entries({
|
|
297
372
|
first_name: "Ada",
|
|
298
373
|
last_name: "Lovelace",
|
|
299
374
|
email: "ada@example.com",
|
|
300
|
-
linkedin_url: "https://www.linkedin.com/in/ada",
|
|
301
375
|
})) {
|
|
302
376
|
fireEvent.change(screen.getByTestId(`field-${name}`), {
|
|
303
377
|
target: { value },
|
|
@@ -318,7 +392,6 @@ describe("a submission from the job page lands as a job application lead (#8746
|
|
|
318
392
|
expect(body.payload).toMatchObject({
|
|
319
393
|
name: "Ada Lovelace",
|
|
320
394
|
job: JOB,
|
|
321
|
-
linkedin_url: "https://www.linkedin.com/in/ada",
|
|
322
395
|
page: "careers",
|
|
323
396
|
});
|
|
324
397
|
// #8746 — the careers catalog's own success line, not /contact's.
|
|
@@ -328,10 +401,11 @@ describe("a submission from the job page lands as a job application lead (#8746
|
|
|
328
401
|
});
|
|
329
402
|
});
|
|
330
403
|
|
|
331
|
-
describe("the /careers index links to each job and carries no form (#8746 D1,
|
|
404
|
+
describe("the /careers index links to each job and carries no form (#8746 D1, #8849 D10)", () => {
|
|
332
405
|
it("renders the list with no apply form and reads no magnet", async () => {
|
|
333
406
|
const element = await CareersPage({
|
|
334
407
|
params: Promise.resolve({ locale: "en" }),
|
|
408
|
+
getMdxSlugs: jest.fn().mockResolvedValue([]),
|
|
335
409
|
getMdxSource: jest.fn().mockResolvedValue({
|
|
336
410
|
frontmatter: {
|
|
337
411
|
type: "CareersList",
|
|
@@ -339,7 +413,7 @@ describe("the /careers index links to each job and carries no form (#8746 D1, AC
|
|
|
339
413
|
title: "Careers",
|
|
340
414
|
description: "Open roles.",
|
|
341
415
|
},
|
|
342
|
-
body: "
|
|
416
|
+
body: "",
|
|
343
417
|
}),
|
|
344
418
|
});
|
|
345
419
|
render(element);
|