This commit contains the latest updates from the generic Website template, which move the toolchain to Swift 6.4, gate analytics and structured data behind traits, and rework page routing: - Updated the Swift toolchain for all the package dependencies and the Website service to version 6.4. (345b305) - Adapted all the existing middlewares in the Infrastructure package to Swift 6.4. (26008df) - Moved test resource teardown in the Persistence and the Website library into defer blocks. (27984a9) - Gated analytics behind a trait and reworked structured data in the Infrastructure package. (c7d4769) - Enabled the Analytics trait in the Website service. (f6a6bbd) - Reworked page routing in the Website service. (d1cb2c1) - Gated the structured data features in the Infrastructure package behind a trait. (f0a8791) - Enabled the StructuredData trait of the Infrastructure package in the Website service. (bbfdeb3) - Fixed the asset pin and a build warning in the Dockerfile of the Website service. (6ed0f9b) The root README change in 345b305 was left out, as this project no longer carries that file. From d1cb2c1, the RootController rework was left out, as the prototype serves a session-aware landing page and routes its other pages through their own controllers; the view-event reporter was appended to the prototype's shared script instead. The prototype code was adapted to the stricter Swift 6.4 settings: `String(format:)` was swapped for safe formatting, the NoIndex middleware takes a `@concurrent` next handler, and the tests flagged by warnings-as-errors were tightened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
8.3 KiB
Swift
220 lines
8.3 KiB
Swift
import Elementary
|
|
import Foundation
|
|
import Testing
|
|
|
|
@testable import Infrastructure
|
|
|
|
@Suite(
|
|
"Page protocol",
|
|
.tags(.`protocol`)
|
|
)
|
|
struct PageTests {
|
|
|
|
// MARK: Functional tests
|
|
|
|
@Test
|
|
func `assembles the document around the page's parts`() {
|
|
let html = StubPage().render()
|
|
|
|
#expect(html.contains("<title>Stub Page</title>"))
|
|
#expect(html.contains(#"lang="en""#))
|
|
#expect(html.contains(#"name="viewport""#))
|
|
#expect(html.contains(#"<meta name="stub" content="marker">"#))
|
|
#expect(html.contains(#"<link rel="stylesheet" href="/css/stub.css">"#))
|
|
#expect(html.contains(#"<script defer src="/js/stub.js"></script>"#))
|
|
#expect(html.contains("Stub content"))
|
|
}
|
|
|
|
@Test
|
|
func `places the metadata between the viewport and the stylesheets`() throws {
|
|
let html = StubPage().render()
|
|
|
|
let viewport = try #require(html.range(of: #"name="viewport""#))
|
|
let metadata = try #require(html.range(of: #"name="stub""#))
|
|
let stylesheet = try #require(html.range(of: "/css/stub.css"))
|
|
|
|
#expect(viewport.lowerBound < metadata.lowerBound)
|
|
#expect(metadata.lowerBound < stylesheet.lowerBound)
|
|
}
|
|
|
|
@Test
|
|
func `renders the scripts deferred in the head, after the stylesheets`() throws {
|
|
let html = StubPage().render()
|
|
|
|
let stylesheet = try #require(html.range(of: "/css/stub.css"))
|
|
let script = try #require(html.range(of: "/js/stub.js"))
|
|
let content = try #require(html.range(of: "Stub content"))
|
|
|
|
// Deferred head scripts start downloading during head parsing but still execute, in order, only after the
|
|
// document is parsed — the semantics end-of-body tags gave, minus the late download start.
|
|
#expect(stylesheet.lowerBound < script.lowerBound)
|
|
#expect(script.lowerBound < content.lowerBound)
|
|
}
|
|
|
|
@Test
|
|
func `omits the summary and canonical tags by default`() {
|
|
let html = StubPage().render()
|
|
|
|
#expect(!html.contains(#"name="description""#))
|
|
#expect(!html.contains(#"rel="canonical""#))
|
|
}
|
|
|
|
@Test
|
|
func `renders the summary and canonical tags when provided`() {
|
|
let html = StubPage(
|
|
canonicalURL: "https://stub.example/",
|
|
summary: "A stub page."
|
|
).render()
|
|
|
|
#expect(html.contains(#"<meta name="description" content="A stub page.">"#))
|
|
#expect(html.contains(#"<link rel="canonical" href="https://stub.example/">"#))
|
|
}
|
|
|
|
@Test
|
|
func `omits the social card tags by default`() {
|
|
let html = StubPage().render()
|
|
|
|
#expect(!html.contains(#"property="og:"#))
|
|
#expect(!html.contains(#"name="twitter:card""#))
|
|
}
|
|
|
|
@Test
|
|
func `renders the social card tags when provided`() {
|
|
let html = StubPage(socialCard: .init(
|
|
title: "Stub Page",
|
|
summary: "A stub page.",
|
|
url: "https://stub.example/",
|
|
image: .init(
|
|
url: "https://stub.example/img/card.png",
|
|
width: 2400,
|
|
height: 1260
|
|
)
|
|
)).render()
|
|
|
|
#expect(html.contains(#"<meta property="og:type" content="website">"#))
|
|
#expect(html.contains(#"<meta property="og:title" content="Stub Page">"#))
|
|
#expect(html.contains(#"<meta property="og:description" content="A stub page.">"#))
|
|
#expect(html.contains(#"<meta property="og:url" content="https://stub.example/">"#))
|
|
#expect(html.contains(#"<meta property="og:image" content="https://stub.example/img/card.png">"#))
|
|
#expect(html.contains(#"<meta property="og:image:width" content="2400">"#))
|
|
#expect(html.contains(#"<meta property="og:image:height" content="1260">"#))
|
|
#expect(html.contains(#"<meta name="twitter:card" content="summary_large_image">"#))
|
|
}
|
|
|
|
#if StructuredData
|
|
@Test
|
|
func `omits the structured data script by default`() {
|
|
let html = StubPage().render()
|
|
|
|
#expect(!html.contains("application/ld+json"))
|
|
}
|
|
|
|
@Test
|
|
func `renders the structured data script when provided`() {
|
|
let html = StubPage(structuredData: .init(
|
|
organization: .init(
|
|
name: "Stub Site",
|
|
url: "https://stub.example/",
|
|
logo: "https://stub.example/logo.png",
|
|
profiles: ["https://social.example/stub"]
|
|
)
|
|
)).render()
|
|
|
|
#expect(html.contains(
|
|
#"<script type="application/ld+json">"# +
|
|
#"{"@context":"https://schema.org","@graph":["# +
|
|
#"{"@type":"Organization","@id":"https://stub.example/#organization","name":"Stub Site","url":"https://stub.example/","logo":"https://stub.example/logo.png","sameAs":["https://social.example/stub"]},"# +
|
|
#"{"@type":"WebSite","@id":"https://stub.example/#website","name":"Stub Site","url":"https://stub.example/","publisher":{"@id":"https://stub.example/#organization"}}]}"# +
|
|
#"</script>"#
|
|
))
|
|
}
|
|
#endif
|
|
|
|
#if Analytics
|
|
@Test
|
|
func `omits the analytics tracker by default`() {
|
|
let html = StubPage().render()
|
|
|
|
#expect(!html.contains("data-website-id"))
|
|
#expect(!html.contains(#"rel="preconnect""#))
|
|
}
|
|
|
|
@Test
|
|
func `renders the analytics tracker when provided`() throws {
|
|
let html = StubPage(analytics: .init(
|
|
scriptURL: "https://analytics.example.com/script",
|
|
websiteID: "0000-website-id",
|
|
domains: "example.com"
|
|
)).render()
|
|
|
|
#expect(html.contains(#"<script defer src="https://analytics.example.com/script" data-website-id="0000-website-id" data-domains="example.com" data-exclude-hash="true" data-do-not-track="true" data-performance="true"></script>"#))
|
|
|
|
// The preconnect hint warms the tracker origin's connection before the parser reaches the script tag.
|
|
let preconnect = try #require(html.range(of: #"<link rel="preconnect" href="https://analytics.example.com">"#))
|
|
let script = try #require(html.range(of: #"<script defer src="https://analytics.example.com/script""#))
|
|
|
|
#expect(preconnect.lowerBound < script.lowerBound)
|
|
}
|
|
|
|
@Test
|
|
func `omits the session recorder script by default`() {
|
|
let html = StubPage(analytics: .init(
|
|
scriptURL: "https://analytics.example.com/script",
|
|
websiteID: "0000-website-id",
|
|
domains: "example.com"
|
|
)).render()
|
|
|
|
#expect(!html.contains("recorder.js"))
|
|
}
|
|
|
|
@Test
|
|
func `renders the session recorder script when recorder mode is on`() throws {
|
|
let html = StubPage(analytics: .init(
|
|
scriptURL: "https://analytics.example.com/script",
|
|
websiteID: "0000-website-id",
|
|
domains: "example.com",
|
|
recorder: true
|
|
)).render()
|
|
|
|
#expect(html.contains(#"<script defer src="https://analytics.example.com/recorder.js" data-website-id="0000-website-id"></script>"#))
|
|
|
|
let tracker = try #require(html.range(of: #"<script defer src="https://analytics.example.com/script""#))
|
|
let recorder = try #require(html.range(of: #"<script defer src="https://analytics.example.com/recorder.js""#))
|
|
|
|
#expect(tracker.lowerBound < recorder.lowerBound)
|
|
}
|
|
|
|
@Test
|
|
func `omits the analytics behavior flags that are disabled`() {
|
|
let html = StubPage(analytics: .init(
|
|
scriptURL: "https://analytics.example.com/script",
|
|
websiteID: "0000-website-id",
|
|
domains: "example.com",
|
|
excludeHash: true,
|
|
doNotTrack: false,
|
|
performance: false
|
|
)).render()
|
|
|
|
#expect(html.contains(#"<script defer src="https://analytics.example.com/script" data-website-id="0000-website-id" data-domains="example.com" data-exclude-hash="true"></script>"#))
|
|
#expect(!html.contains("data-do-not-track"))
|
|
#expect(!html.contains("data-performance"))
|
|
}
|
|
#endif
|
|
|
|
@Test
|
|
func `appends the version token to the asset URLs`() {
|
|
let html = StubPage(assetVersion: "0123456789abcdef").render()
|
|
|
|
#expect(html.contains("/css/stub.css?v=0123456789abcdef"))
|
|
#expect(html.contains("/js/stub.js?v=0123456789abcdef"))
|
|
}
|
|
|
|
@Test
|
|
func `derives the document language from the locale`() {
|
|
let html = StubPage(locale: .init(identifier: "de-DE")).render()
|
|
|
|
#expect(html.contains(#"lang="de""#))
|
|
}
|
|
|
|
}
|