11import { expect , test } from "@playwright/test" ;
2+ import { DONOR_EMAIL , SEEDED_THREAD_ID , signIn } from "./helpers" ;
23
34const LISTING_PATH = "/listings/demo-marrickville-compost" ;
45const MAP_LISTING_PATH = "/map?listing=demo-marrickville-compost" ;
56const RESIDENTIAL_LISTING_PATH = "/listings/demo-newtown-worm-farm" ;
7+ const SITE_URL = "https://www.peels.app" ;
8+ const DEFAULT_OG_IMAGE_PATTERN =
9+ / ^ h t t p s : \/ \/ w w w \. p e e l s \. a p p \/ o p e n g r a p h - i m a g e \. j p g / ;
610
711async function getMetaDescription ( page : import ( "@playwright/test" ) . Page ) {
812 return page . locator ( 'head meta[name="description"]' ) . getAttribute ( "content" ) ;
@@ -12,6 +16,35 @@ async function getListingJsonLdScripts(page: import("@playwright/test").Page) {
1216 return page . locator ( 'script[type="application/ld+json"]' ) . allTextContents ( ) ;
1317}
1418
19+ async function expectSharedSocialMetadata (
20+ page : import ( "@playwright/test" ) . Page ,
21+ canonicalPath : string
22+ ) {
23+ const canonicalUrl =
24+ canonicalPath === "/" ? SITE_URL : `${ SITE_URL } ${ canonicalPath } ` ;
25+
26+ await expect ( page . locator ( 'head link[rel="canonical"]' ) ) . toHaveAttribute (
27+ "href" ,
28+ canonicalUrl
29+ ) ;
30+ await expect ( page . locator ( 'head meta[property="og:url"]' ) ) . toHaveAttribute (
31+ "content" ,
32+ canonicalUrl
33+ ) ;
34+ await expect ( page . locator ( 'head meta[property="og:image"]' ) ) . toHaveAttribute (
35+ "content" ,
36+ DEFAULT_OG_IMAGE_PATTERN
37+ ) ;
38+ await expect ( page . locator ( 'head meta[name="twitter:card"]' ) ) . toHaveAttribute (
39+ "content" ,
40+ "summary_large_image"
41+ ) ;
42+ await expect ( page . locator ( 'head meta[name="twitter:image"]' ) ) . toHaveAttribute (
43+ "content" ,
44+ DEFAULT_OG_IMAGE_PATTERN
45+ ) ;
46+ }
47+
1548type ListingJsonLd = {
1649 "@context" ?: unknown ;
1750 "@type" ?: unknown ;
@@ -57,25 +90,73 @@ test("homepage emits object-shaped site JSON-LD", async ({ page }) => {
5790 await page . goto ( "/" , { waitUntil : "domcontentloaded" } ) ;
5891
5992 const jsonLdScripts = await getListingJsonLdScripts ( page ) ;
60- const siteJsonLd = parseJsonLdScripts ( jsonLdScripts ) . find ( ( data ) =>
61- Array . isArray ( data [ "@graph" ] )
93+ const siteJsonLd = parseJsonLdScripts ( jsonLdScripts ) ;
94+ const organizationJsonLd = siteJsonLd . find (
95+ ( data ) => data [ "@type" ] === "Organization"
6296 ) ;
97+ const websiteJsonLd = siteJsonLd . find ( ( data ) => data [ "@type" ] === "WebSite" ) ;
6398
64- expect ( siteJsonLd ) . toEqual (
99+ expect ( organizationJsonLd ) . toEqual (
65100 expect . objectContaining ( {
66101 "@context" : "https://schema.org" ,
67- "@graph" : expect . arrayContaining ( [
68- expect . objectContaining ( {
69- "@type" : "Organization" ,
70- name : "Peels" ,
71- } ) ,
72- expect . objectContaining ( {
73- "@type" : "WebSite" ,
74- name : "Peels" ,
75- } ) ,
76- ] ) ,
102+ "@type" : "Organization" ,
103+ name : "Peels" ,
77104 } )
78105 ) ;
106+ expect ( websiteJsonLd ) . toEqual (
107+ expect . objectContaining ( {
108+ "@context" : "https://schema.org" ,
109+ "@type" : "WebSite" ,
110+ name : "Peels" ,
111+ } )
112+ ) ;
113+ } ) ;
114+
115+ test ( "homepage exposes canonical social metadata and one primary H1" , async ( {
116+ page,
117+ } ) => {
118+ await page . goto ( "/" , { waitUntil : "domcontentloaded" } ) ;
119+
120+ await expect ( page ) . toHaveTitle (
121+ "Peels: Find a home for your food scraps, wherever you are"
122+ ) ;
123+ await expectSharedSocialMetadata ( page , "/" ) ;
124+ await expect ( page . getByRole ( "heading" , { level : 1 } ) ) . toHaveText ( [
125+ "Find a home for your food scraps, wherever you are" ,
126+ ] ) ;
127+
128+ const emptyLinks = await page . locator ( "a" ) . evaluateAll ( ( links ) =>
129+ links
130+ . filter ( ( link ) => {
131+ const text = link . textContent ?. trim ( ) ;
132+ const ariaLabel = link . getAttribute ( "aria-label" ) ?. trim ( ) ;
133+ const imageAlts = Array . from ( link . querySelectorAll ( "img" ) )
134+ . map ( ( image ) => image . getAttribute ( "alt" ) ?. trim ( ) )
135+ . filter ( Boolean ) ;
136+
137+ return ! text && ! ariaLabel && imageAlts . length === 0 ;
138+ } )
139+ . map ( ( link ) => link . outerHTML )
140+ ) ;
141+
142+ expect ( emptyLinks ) . toEqual ( [ ] ) ;
143+ } ) ;
144+
145+ test ( "public static pages expose canonical social metadata" , async ( {
146+ page,
147+ } ) => {
148+ for ( const { path, title } of [
149+ { path : "/map" , title : "Map · Peels" } ,
150+ { path : "/newsletter" , title : "Newsletter · Peels" } ,
151+ { path : "/help" , title : "Help · Peels" } ,
152+ { path : "/partners" , title : "Partners · Peels" } ,
153+ { path : "/share" , title : "Share · Peels" } ,
154+ ] ) {
155+ await page . goto ( path , { waitUntil : "domcontentloaded" } ) ;
156+
157+ await expect ( page ) . toHaveTitle ( title ) ;
158+ await expectSharedSocialMetadata ( page , path ) ;
159+ }
79160} ) ;
80161
81162test ( "homepage emits summary FAQPage JSON-LD" , async ( { page } ) => {
@@ -104,11 +185,8 @@ test("public listing pages expose crawlable listing metadata", async ({
104185} ) => {
105186 await page . goto ( LISTING_PATH , { waitUntil : "domcontentloaded" } ) ;
106187
107- await expect ( page ) . toHaveTitle ( "Marrickville Neighbourhood Compost" ) ;
108- await expect ( page . locator ( 'head link[rel="canonical"]' ) ) . toHaveAttribute (
109- "href" ,
110- / \/ l i s t i n g s \/ d e m o - m a r r i c k v i l l e - c o m p o s t $ /
111- ) ;
188+ await expect ( page ) . toHaveTitle ( "Marrickville Neighbourhood Compost · Peels" ) ;
189+ await expectSharedSocialMetadata ( page , LISTING_PATH ) ;
112190
113191 const description = await getMetaDescription ( page ) ;
114192 expect ( description ) . toContain (
@@ -148,11 +226,8 @@ test("anonymous residential listing pages expose an indexable private-host tease
148226} ) => {
149227 await page . goto ( RESIDENTIAL_LISTING_PATH , { waitUntil : "domcontentloaded" } ) ;
150228
151- await expect ( page ) . toHaveTitle ( "Private Host" ) ;
152- await expect ( page . locator ( 'head link[rel="canonical"]' ) ) . toHaveAttribute (
153- "href" ,
154- / \/ l i s t i n g s \/ d e m o - n e w t o w n - w o r m - f a r m $ /
155- ) ;
229+ await expect ( page ) . toHaveTitle ( "Private Host · Peels" ) ;
230+ await expectSharedSocialMetadata ( page , RESIDENTIAL_LISTING_PATH ) ;
156231 await expect ( page . locator ( 'head meta[name="robots"]' ) ) . toHaveCount ( 0 ) ;
157232
158233 const description = await getMetaDescription ( page ) ;
@@ -213,7 +288,9 @@ test("public listing pages localise Spanish SEO metadata", async ({
213288 await page . goto ( LISTING_PATH , { waitUntil : "domcontentloaded" } ) ;
214289
215290 await expect ( page . locator ( "html" ) ) . toHaveAttribute ( "lang" , "es" ) ;
216- await expect ( page ) . toHaveTitle ( "Marrickville Neighbourhood Compost" ) ;
291+ await expect ( page ) . toHaveTitle (
292+ "Marrickville Neighbourhood Compost · Peels"
293+ ) ;
217294 await expect ( page . locator ( 'head link[rel="canonical"]' ) ) . toHaveAttribute (
218295 "href" ,
219296 / \/ l i s t i n g s \/ d e m o - m a r r i c k v i l l e - c o m p o s t $ /
@@ -269,7 +346,7 @@ test("anonymous residential listing pages localise the private-host teaser", asy
269346 } ) ;
270347
271348 await expect ( page . locator ( "html" ) ) . toHaveAttribute ( "lang" , "es" ) ;
272- await expect ( page ) . toHaveTitle ( "Anfitrión privado" ) ;
349+ await expect ( page ) . toHaveTitle ( "Anfitrión privado · Peels " ) ;
273350 await expect (
274351 page . getByRole ( "heading" , { name : "Anfitrión privado" } )
275352 ) . toBeVisible ( ) ;
@@ -313,10 +390,7 @@ test("map listing URLs canonicalise to the static listing sibling", async ({
313390} ) => {
314391 await page . goto ( MAP_LISTING_PATH , { waitUntil : "domcontentloaded" } ) ;
315392
316- await expect ( page . locator ( 'head link[rel="canonical"]' ) ) . toHaveAttribute (
317- "href" ,
318- / \/ l i s t i n g s \/ d e m o - m a r r i c k v i l l e - c o m p o s t $ /
319- ) ;
393+ await expectSharedSocialMetadata ( page , LISTING_PATH ) ;
320394} ) ;
321395
322396test ( "map listing URLs localise metadata without changing canonical" , async ( {
@@ -331,10 +405,7 @@ test("map listing URLs localise metadata without changing canonical", async ({
331405 try {
332406 await page . goto ( MAP_LISTING_PATH , { waitUntil : "domcontentloaded" } ) ;
333407
334- await expect ( page . locator ( 'head link[rel="canonical"]' ) ) . toHaveAttribute (
335- "href" ,
336- / \/ l i s t i n g s \/ d e m o - m a r r i c k v i l l e - c o m p o s t $ /
337- ) ;
408+ await expectSharedSocialMetadata ( page , LISTING_PATH ) ;
338409
339410 const description = await getMetaDescription ( page ) ;
340411 expect ( description ) . toContain (
@@ -384,6 +455,7 @@ test("auth utility pages are noindex and omitted from the sitemap", async ({
384455 "content" ,
385456 / n o i n d e x , \s * f o l l o w /
386457 ) ;
458+ await expectSharedSocialMetadata ( page , "/sign-in" ) ;
387459
388460 const sitemap = await request . get ( "/sitemap.xml" ) ;
389461 expect ( sitemap . ok ( ) ) . toBeTruthy ( ) ;
@@ -394,6 +466,37 @@ test("auth utility pages are noindex and omitted from the sitemap", async ({
394466 expect ( sitemapXml ) . toContain ( "/listings/demo-marrickville-compost" ) ;
395467} ) ;
396468
469+ test ( "signed-in private pages keep noindex metadata and route-specific titles" , async ( {
470+ page,
471+ } ) => {
472+ await signIn ( page , { email : DONOR_EMAIL , redirectTo : "/profile" } ) ;
473+
474+ await expect ( page ) . toHaveTitle ( "Profile · Peels" ) ;
475+ await expect ( page . locator ( 'head meta[name="robots"]' ) ) . toHaveAttribute (
476+ "content" ,
477+ / n o i n d e x , \s * f o l l o w /
478+ ) ;
479+ await expectSharedSocialMetadata ( page , "/profile" ) ;
480+
481+ await page . goto ( "/chats" , { waitUntil : "domcontentloaded" } ) ;
482+ await expect ( page ) . toHaveTitle ( "Chats · Peels" ) ;
483+ await expect ( page . locator ( 'head meta[name="robots"]' ) ) . toHaveAttribute (
484+ "content" ,
485+ / n o i n d e x , \s * f o l l o w /
486+ ) ;
487+ await expectSharedSocialMetadata ( page , "/chats" ) ;
488+
489+ await page . goto ( `/chats/${ SEEDED_THREAD_ID } ` , {
490+ waitUntil : "domcontentloaded" ,
491+ } ) ;
492+ await expect ( page ) . toHaveTitle ( "Avery · Chats · Peels" ) ;
493+ await expect ( page . locator ( 'head meta[name="robots"]' ) ) . toHaveAttribute (
494+ "content" ,
495+ / n o i n d e x , \s * f o l l o w /
496+ ) ;
497+ await expectSharedSocialMetadata ( page , `/chats/${ SEEDED_THREAD_ID } ` ) ;
498+ } ) ;
499+
397500test ( "help page emits FAQPage JSON-LD for visible help questions" , async ( {
398501 page,
399502} ) => {
0 commit comments