Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 18 additions & 32 deletions app/[lang]/(hyperjump)/oneai/get-oneai-comparison-rows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,46 +25,32 @@ describe("isNegativeComparisonValue", () => {

it("returns false for positive or neutral values", () => {
expect(isNegativeComparisonValue("Yes · all models")).toBe(false);
expect(isNegativeComparisonValue("Rp12,400,000")).toBe(false);
expect(isNegativeComparisonValue("Included")).toBe(false);
});
});

describe("getOneaiComparisonRows", () => {
it("returns eleven localized rows for English", () => {
it("returns capability rows without cost comparisons for English", () => {
const rows = getOneaiComparisonRows({ lang: "en" });

expect(rows).toHaveLength(11);
expect(rows[0]?.feature).toBe("20 users");
expect(rows[0]?.advantage).toBeNull();
expect(rows[0]?.chatgpt).toContain("annual");
expect(rows[0]?.copilot).toContain("M365");
expect(rows[1]?.advantage).toBeNull();
expect(rows[1]?.chatgpt).toContain("Rp12,800,000");
expect(rows[2]?.chatgpt).toContain("included");
expect(rows[2]?.chatgpt).not.toMatch(/bought separately/i);
expect(rows[2]?.copilot).toContain("add-on");
expect(rows[3]?.feature).toContain("across providers");
expect(rows[9]?.chatgpt).toContain("Yes");
expect(rows[9]?.oneai).toContain("Yes");
expect(rows[9]?.advantage).toBeNull();
expect(rows[10]?.feature).toContain("On-prem");
expect(rows[10]?.oneai).toBe("Yes");
expect(rows[10]?.copilot).toContain("cloud-only");
expect(rows[10]?.advantage).toBe("capability");
expect(rows).toHaveLength(9);
expect(rows[0]?.feature).toContain("Usage budget");
expect(rows[0]?.oneai).toContain("Shared budget");
expect(rows[0]?.chatgpt).toContain("included");
expect(rows[8]?.feature).toContain("On-prem");
expect(rows[8]?.oneai).toBe("Yes");
expect(rows[8]?.copilot).toContain("cloud-only");
expect(rows[8]?.advantage).toBe("capability");
});

it("returns eleven localized rows for Indonesian", () => {
it("returns capability rows without cost comparisons for Indonesian", () => {
const rows = getOneaiComparisonRows({ lang: "id" });

expect(rows).toHaveLength(11);
expect(rows[0]?.feature).toBe("20 pengguna");
expect(rows[1]?.oneai).toContain("harga tetap");
expect(rows[1]?.chatgpt).toContain("tahunan");
expect(rows[1]?.copilot).toContain("M365");
expect(rows[2]?.chatgpt).not.toMatch(/dibeli terpisah/i);
expect(rows[3]?.advantage).toBe("capability");
expect(rows[10]?.feature).toContain("On-premise");
expect(rows[10]?.copilot).toContain("cloud saja");
expect(rows).toHaveLength(9);
expect(rows[0]?.feature).toContain("Anggaran pemakaian");
expect(rows[0]?.oneai).toContain("Satu anggaran");
expect(rows[8]?.feature).toContain("On-premise");
expect(rows[8]?.copilot).toContain("cloud saja");
});

it("uses injected row loaders when provided", () => {
Expand Down Expand Up @@ -95,7 +81,7 @@ describe("getOneaiComparisonRows", () => {
});

it("exports default row loaders for each comparison row", () => {
expect(DEFAULT_ROW_LOADERS).toHaveLength(11);
expect(DEFAULT_ROW_LOADERS).toHaveLength(9);
expect(
DEFAULT_ROW_LOADERS.filter((row) => row.advantage === "price")
).toHaveLength(0);
Expand All @@ -119,7 +105,7 @@ describe("getOneaiComparisonWhy", () => {

expect(columns).toHaveLength(3);
expect(columns[0]?.title).toContain("Allowance");
expect(columns[1]?.title).toContain("kursi");
expect(columns[1]?.title).toContain("Kursi");
});

it("uses injected why loaders when provided", () => {
Expand Down
26 changes: 0 additions & 26 deletions app/[lang]/(hyperjump)/oneai/get-oneai-comparison-rows.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
import type { SupportedLanguage } from "@/locales/.generated/types";
import {
oneaiComparisonRows0Chatgpt,
oneaiComparisonRows0Copilot,
oneaiComparisonRows0Feature,
oneaiComparisonRows0Oneai,
oneaiComparisonRows0Stipend,
oneaiComparisonRows1Chatgpt,
oneaiComparisonRows1Copilot,
oneaiComparisonRows1Feature,
oneaiComparisonRows1Oneai,
oneaiComparisonRows1Stipend,
oneaiComparisonRows2Chatgpt,
oneaiComparisonRows2Copilot,
oneaiComparisonRows2Feature,
Expand Down Expand Up @@ -104,22 +94,6 @@ const NEGATIVE_COMPARISON_VALUE_PATTERN =
/^(No|Tidak|Limited|Terbatas|Not ZDR|Bukan ZDR|Tergantung|Depends)/i;

const DEFAULT_ROW_LOADERS: RowLoader[] = [
{
feature: oneaiComparisonRows0Feature,
stipend: oneaiComparisonRows0Stipend,
chatgpt: oneaiComparisonRows0Chatgpt,
copilot: oneaiComparisonRows0Copilot,
oneai: oneaiComparisonRows0Oneai,
advantage: null
},
{
feature: oneaiComparisonRows1Feature,
stipend: oneaiComparisonRows1Stipend,
chatgpt: oneaiComparisonRows1Chatgpt,
copilot: oneaiComparisonRows1Copilot,
oneai: oneaiComparisonRows1Oneai,
advantage: null
},
{
feature: oneaiComparisonRows2Feature,
stipend: oneaiComparisonRows2Stipend,
Expand Down
14 changes: 6 additions & 8 deletions app/[lang]/(hyperjump)/oneai/get-oneai-json-ld.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ describe("getDefaultOneaiFaqs", () => {
expect(faqs[0]?.question).toContain("users");
expect(faqs[1]?.question).toContain("more than 40");
expect(faqs[1]?.answer).toContain("Contact us");
expect(faqs[3]?.question).toContain("USD 300");
expect(faqs[3]?.answer).toContain("overage");
expect(faqs[3]?.question).toContain("usage reaches its limit");
expect(faqs[3]?.answer).toContain("additional capacity");
expect(faqs[5]?.question).toContain("installed");
expect(faqs[5]?.answer).toContain("premises");
expect(faqs[5]?.answer).toContain("isolated");
Expand All @@ -43,8 +43,8 @@ describe("getDefaultOneaiFaqs", () => {
expect(faqs[0]?.question).toContain("pengguna");
expect(faqs[1]?.question).toContain("lebih dari 40");
expect(faqs[1]?.answer).toContain("Hubungi kami");
expect(faqs[3]?.question).toContain("USD 300");
expect(faqs[3]?.answer).toContain("overage");
expect(faqs[3]?.question).toContain("mencapai batasnya");
expect(faqs[3]?.answer).toContain("kapasitas tambahan");
expect(faqs[5]?.question).toContain("dipasang");
expect(faqs[5]?.answer).toContain("infrastruktur");
expect(faqs[5]?.answer).toContain("terisolasi");
Expand Down Expand Up @@ -79,9 +79,7 @@ describe("getOneaiJsonLd", () => {
expect(product.name).toBe("OneAI");
expect(product.operatingSystem).toBe("On-premises, Web");
expect(product.image).toBe(`${siteUrl}${ONEAI_OG_IMAGE_PATH}`);
expect(product.offers.price).toBe("12400000");
expect(product.offers.priceCurrency).toBe("IDR");
expect(product.offers.description).toContain("before tax");
expect(product).not.toHaveProperty("offers");

const faqPage = graph["@graph"][2];
expect(faqPage["@type"]).toBe("FAQPage");
Expand All @@ -97,7 +95,7 @@ describe("getOneaiJsonLd", () => {

const breadcrumb = graph["@graph"][0];
expect(breadcrumb.itemListElement[0].name).toBe("Beranda");
expect(graph["@graph"][1].offers.description).toContain("sebelum pajak");
expect(graph["@graph"][1]).not.toHaveProperty("offers");
});
});

Expand Down
12 changes: 1 addition & 11 deletions app/[lang]/(hyperjump)/oneai/get-oneai-json-ld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const ONEAI_OG_IMAGE_PATH = "/images/oneai/og.png";
* Returns structured data graphs for the OneAI landing page.
*
* @param options - Locale, URLs, and optional string loaders for DI
* @returns JSON-LD graph with SoftwareApplication, Offer, FAQPage, and BreadcrumbList
* @returns JSON-LD graph with SoftwareApplication, FAQPage, and BreadcrumbList
*/
export function getOneaiJsonLd({
lang,
Expand Down Expand Up @@ -85,16 +85,6 @@ export function getOneaiJsonLd({
description,
url: pageUrl,
image: `${siteUrl}${ONEAI_OG_IMAGE_PATH}`,
offers: {
"@type": "Offer",
price: "12400000",
priceCurrency: "IDR",
description:
lang === "id"
? "Paket hingga 40 pengguna, ditagih per kuartal, sebelum pajak"
: "Plan for up to 40 users, billed quarterly, before tax",
url: pageUrl
},
provider: {
"@type": "Organization",
name: "Hyperjump Technology",
Expand Down
82 changes: 0 additions & 82 deletions app/[lang]/(hyperjump)/oneai/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,6 @@ import {
oneaiPillars2Text,
oneaiPillars2Title,
oneaiPillarsLabel,
oneaiPricingAnnual,
oneaiPricingCompareItems0,
oneaiPricingCompareItems1,
oneaiPricingCompareItems2,
oneaiPricingCompareItems3,
oneaiPricingCompareKicker,
oneaiPricingCompareTitle,
oneaiPricingCta,
oneaiPricingIncludes,
oneaiPricingLabel,
oneaiPricingPrice,
oneaiPricingPriceUnit,
oneaiPricingQuarterly,
oneaiStickyCta
} from "@/locales/.generated/strings";

Expand Down Expand Up @@ -214,13 +201,6 @@ export default async function OneaiPage({ params }: OneaiPageProps) {
text: oneaiCapabilities7Text(lang)
}
];
const compareItems = [
oneaiPricingCompareItems0(lang),
oneaiPricingCompareItems1(lang),
oneaiPricingCompareItems2(lang),
oneaiPricingCompareItems3(lang)
];

return (
<main className="pb-24 md:pb-0">
<script
Expand Down Expand Up @@ -342,68 +322,6 @@ export default async function OneaiPage({ params }: OneaiPageProps) {
</div>
</section>

<section
id="pricing"
data-testid="oneai-pricing"
className="bg-cta-premium relative overflow-hidden py-16 text-white md:py-24">
<div className="relative z-10 mx-auto max-w-5xl px-4 md:px-20 xl:px-0">
<div className="grid gap-10 lg:grid-cols-2 lg:gap-12">
<SectionReveal>
<p className="text-hyperjump-teal mb-4 text-xs font-bold tracking-[0.16em] uppercase">
{oneaiPricingLabel(lang)}
</p>
<p
className="mb-2 text-4xl font-bold tracking-tight md:text-5xl"
data-testid="oneai-price">
{oneaiPricingPrice(lang)}
<span className="ml-2 text-lg font-medium text-white/70">
{oneaiPricingPriceUnit(lang)}
</span>
</p>
<p className="mb-4 text-base text-white/75">
{oneaiPricingQuarterly(lang)}
</p>
<span className="bg-hyperjump-blue/20 text-hyperjump-teal mb-6 inline-block rounded-full px-4 py-1.5 text-sm font-semibold">
{oneaiPricingAnnual(lang)}
</span>
<p className="mb-8 text-sm leading-relaxed text-white/60">
{oneaiPricingIncludes(lang)}
</p>
<Button
asChild
className="bg-hyperjump-blue hover:bg-hyperjump-blue/90 h-12 rounded-full px-8 text-base font-semibold">
<Link href={mailtoHref} data-testid="oneai-pricing-cta">
<MailIcon className="mr-2 h-4 w-4" aria-hidden />
{oneaiPricingCta(lang)}
</Link>
</Button>
</SectionReveal>

<SectionReveal delay={0.1}>
<p className="text-hyperjump-teal mb-3 text-xs font-bold tracking-[0.16em] uppercase">
{oneaiPricingCompareKicker(lang)}
</p>
<h2
className="mb-6 text-2xl font-semibold md:text-3xl"
dangerouslySetInnerHTML={{
__html: oneaiPricingCompareTitle(lang)
}}
/>
<ul className="space-y-4">
{compareItems.map((item) => (
<li
key={item}
className="flex items-start gap-3 border-b border-white/10 pb-4 text-white/85 last:border-0">
<span className="mt-2 h-0.5 w-4 shrink-0 bg-red-400" />
{item}
</li>
))}
</ul>
</SectionReveal>
</div>
</div>
</section>

<OneaiComparisonSection lang={lang} />

<OneaiSupportSection lang={lang} mailtoHref={mailtoHref} />
Expand Down
12 changes: 5 additions & 7 deletions e2e/oneai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {
oneaiSupportHeading,
oneaiMailtoSubject,
oneaiMetaDescription,
oneaiMetaTitle,
oneaiPricingPrice
oneaiMetaTitle
} from "@/locales/.generated/strings";
import { supportedLanguages } from "@/locales/.generated/types";

Expand Down Expand Up @@ -66,16 +65,15 @@ for (const locale of supportedLanguages) {
expect(ogImage).toContain("/images/oneai/og.png");
});

test("shows pricing amount", async ({ page }) => {
await expect(page.getByTestId("oneai-price")).toHaveText(
new RegExp(oneaiPricingPrice(locale).replace(/\./g, "\\."))
);
test("does not expose a price section", async ({ page }) => {
await expect(page.getByTestId("oneai-pricing")).toHaveCount(0);
await expect(page.getByTestId("oneai-price")).toHaveCount(0);
});

test("mailto CTA includes prefilled subject", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
const href = await page
.getByTestId("oneai-pricing-cta")
.getByTestId("oneai-hero-cta")
.getAttribute("href");

expect(href).toContain("mailto:solution@hyperjump.tech");
Expand Down
37 changes: 21 additions & 16 deletions e2e/shared-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,22 +126,27 @@ async function expectAllImagesLoaded(page: Page) {

await expect(el, `Image #${i} not visible`).toBeVisible();

const { isLoaded, src } = await el.evaluate((img: any) => {
const src = img.currentSrc || img.src || img.href?.baseVal || "(none)";
const isSvgFile = typeof src === "string" && src.endsWith(".svg");
const rect = img.getBoundingClientRect();
const hasLayoutSize = rect.width > 0 && rect.height > 0;
const hasRasterSize =
(img.naturalWidth ?? 0) > 0 && (img.naturalHeight ?? 0) > 0;
const isLoaded = hasRasterSize || (isSvgFile && hasLayoutSize);

return { isLoaded, src };
});

expect(
isLoaded,
`Image failed to load or has zero size: ${src}`
).toBeTruthy();
await expect
.poll(
() =>
el.evaluate((img: any) => {
const src =
img.currentSrc || img.src || img.href?.baseVal || "(none)";
const isSvgFile = typeof src === "string" && src.endsWith(".svg");
const rect = img.getBoundingClientRect();
const hasLayoutSize = rect.width > 0 && rect.height > 0;
const hasRasterSize =
(img.naturalWidth ?? 0) > 0 && (img.naturalHeight ?? 0) > 0;

return hasRasterSize || (isSvgFile && hasLayoutSize);
}),
{
message:
"Image did not finish loading after it entered the viewport",
timeout: 10_000
}
)
.toBeTruthy();
}
}
}
Expand Down
Loading
Loading