Improved the static assets definitions in the Website service (#18)
This PR contains the work done to overhaul the static asset definitions for the Website service.
To provide further details about the work:
* Website library
* Overhauled the `StateFile` enumeration to reduce the number of cases to one case per logical file name, each exposing a `fileExtensions` list.
* Wired everything into the pages with a consistent ordering convention: stylesheets load shared-first so the page sheet wins the CSS cascade; scripts load page-first with shared.js last. The error page also gained the shared stylesheet and its scripts; the index page gained its page CSS/JS and the new touch icon link.
* Used the `StaticFile` enumeration as a single source of truth for every _href_/_src_ in the `IndexPage` and the `ErrorPage` pages, eliminating hardcoded asset paths.
* Website service
* Added new assets to the Resources folder:
* `apple-touch-icon.png`
* `css/index.css`
* `js/index.js`
* `js/error.js`
* `sitemap.xml`
* Renamed existing assets within the Resources folder:
* `css/style.css` → `css/shared.css`
* `js/app.js` → `js/shared.js`
* Fixed the working-directory location for the scheme in the Xcode project.
Reviewed-on: rock-n-code/loud-amsterdam#18
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
@@ -50,7 +50,8 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
useCustomWorkingDirectory = "YES"
|
||||
customWorkingDirectory = "/Users/logan/Documents/Development/Platforms/Röck+Cöde/Loud/Services/Website"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
@@ -3,3 +3,5 @@
|
||||
# Allow crawling of all content
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
||||
Sitemap: https://loud.amsterdam/sitemap.xml
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://loud.amsterdam/</loc>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -1,24 +1,27 @@
|
||||
/// A static file shipped with the website service.
|
||||
///
|
||||
/// Each case identifies a file stored under the static files root (the `Resources/Static`
|
||||
/// directory) and served by Hummingbird's `FileMiddleware` middleware.
|
||||
/// Each case identifies a file name stored under the static files root (the `Resources/Static`
|
||||
/// directory) and served by Hummingbird's `FileMiddleware` middleware. A name can be available
|
||||
/// with more than one extension (see ``fileExtensions``), each resolving to its own file.
|
||||
enum StaticFile: CaseIterable, Sendable {
|
||||
/// The `js/app.js` script.
|
||||
case appJS
|
||||
/// The `css/error.css` stylesheet for the not-found page.
|
||||
case errorCSS
|
||||
/// The `apple-touch-icon.png` icon.
|
||||
case appleTouchIcon
|
||||
/// The `css/error.css` stylesheet and `js/error.js` script for the not-found page.
|
||||
case error
|
||||
/// The `favicon.ico` icon.
|
||||
case faviconICO
|
||||
/// The `icon.png` icon.
|
||||
case iconPNG
|
||||
/// The `icon.svg` icon.
|
||||
case iconSVG
|
||||
case favicon
|
||||
/// The `icon.png` and `icon.svg` icons.
|
||||
case icon
|
||||
/// The `css/index.css` stylesheet and `js/index.js` script for the landing page.
|
||||
case index
|
||||
/// The `robots.txt` crawler directives.
|
||||
case robotsTXT
|
||||
case robots
|
||||
/// The `css/shared.css` stylesheet and `js/shared.js` script shared across pages.
|
||||
case shared
|
||||
/// The `site.webmanifest` web application manifest.
|
||||
case siteWebmanifest
|
||||
/// The `css/style.css` stylesheet.
|
||||
case styleCSS
|
||||
case site
|
||||
/// The `sitemap.xml` crawler sitemap.
|
||||
case sitemap
|
||||
}
|
||||
|
||||
// MARK: - Enumerations
|
||||
@@ -40,6 +43,8 @@ extension StaticFile {
|
||||
case txt
|
||||
/// A web application manifest file.
|
||||
case webmanifest
|
||||
/// An Extensible Markup Language file.
|
||||
case xml
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +54,91 @@ extension StaticFile {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The file extensions the file is available with.
|
||||
var fileExtensions: [Extension] {
|
||||
switch self {
|
||||
case .appleTouchIcon: [.png]
|
||||
case .error,
|
||||
.index,
|
||||
.shared: [.css, .js]
|
||||
case .favicon: [.ico]
|
||||
case .icon: [.png, .svg]
|
||||
case .robots: [.txt]
|
||||
case .site: [.webmanifest]
|
||||
case .sitemap: [.xml]
|
||||
}
|
||||
}
|
||||
|
||||
/// The file's name, without extension.
|
||||
var fileName: String {
|
||||
switch self {
|
||||
case .appleTouchIcon: "apple-touch-icon"
|
||||
case .error: "error"
|
||||
case .favicon: "favicon"
|
||||
case .icon: "icon"
|
||||
case .index: "index"
|
||||
case .robots: "robots"
|
||||
case .shared: "shared"
|
||||
case .site: "site"
|
||||
case .sitemap: "sitemap"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Resolves the file's path against the given base directory.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - basePath: the directory the static files are served from.
|
||||
/// - fileExtension: the extension of the file to resolve.
|
||||
/// - Returns: the path to the file, relative to the `basePath` path.
|
||||
func path(
|
||||
relativeTo basePath: String,
|
||||
for fileExtension: Extension
|
||||
) -> String {
|
||||
let relativePath = relativePath(for: fileExtension)
|
||||
|
||||
guard !basePath.isEmpty else {
|
||||
return relativePath
|
||||
}
|
||||
|
||||
return "\(basePath)/\(relativePath)"
|
||||
}
|
||||
|
||||
/// Resolves the file's path relative to the static files root (e.g. `"css/shared.css"`).
|
||||
///
|
||||
/// This also matches the URL path the file is served at by `FileMiddleware`.
|
||||
///
|
||||
/// - Parameter fileExtension: the extension of the file to resolve.
|
||||
/// - Returns: the path to the file, relative to the static files root.
|
||||
func relativePath(
|
||||
for fileExtension: Extension
|
||||
) -> String {
|
||||
let file = "\(fileName).\(fileExtension.rawValue)"
|
||||
|
||||
return fileExtension.subdirectory
|
||||
.map { "\($0)/\(file)" } ?? file
|
||||
}
|
||||
|
||||
/// Resolves the absolute URL path the file is served at (e.g. `"/css/shared.css"`).
|
||||
///
|
||||
/// - Parameter fileExtension: the extension of the file to resolve.
|
||||
/// - Returns: the path to use in `href` and `src` attributes.
|
||||
func urlPath(
|
||||
for fileExtension: Extension
|
||||
) -> String {
|
||||
"/\(relativePath(for: fileExtension))"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StaticFile.Extension {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The file's content type.
|
||||
var contentType: String {
|
||||
switch fileExtension {
|
||||
switch self {
|
||||
case .css: "text/css"
|
||||
case .js: "text/javascript"
|
||||
case .png: "image/png"
|
||||
@@ -59,77 +146,15 @@ extension StaticFile {
|
||||
case .svg: "image/svg+xml"
|
||||
case .txt: "text/plain"
|
||||
case .webmanifest: "application/manifest+json"
|
||||
case .xml: "application/xml"
|
||||
}
|
||||
}
|
||||
|
||||
/// The file's extension.
|
||||
var fileExtension: Extension {
|
||||
switch self {
|
||||
case .errorCSS,
|
||||
.styleCSS: .css
|
||||
case .appJS: .js
|
||||
case .faviconICO: .ico
|
||||
case .iconPNG: .png
|
||||
case .iconSVG: .svg
|
||||
case .robotsTXT: .txt
|
||||
case .siteWebmanifest: .webmanifest
|
||||
}
|
||||
}
|
||||
|
||||
/// The file's name, without extension.
|
||||
var fileName: String {
|
||||
switch self {
|
||||
case .appJS: "app"
|
||||
case .errorCSS: "error"
|
||||
case .faviconICO: "favicon"
|
||||
case .iconPNG,
|
||||
.iconSVG: "icon"
|
||||
case .robotsTXT: "robots"
|
||||
case .siteWebmanifest: "site"
|
||||
case .styleCSS: "style"
|
||||
}
|
||||
}
|
||||
|
||||
/// The path relative to the static files root (e.g. `"css/style.css"`).
|
||||
///
|
||||
/// This also matches the URL path the file is served at by `FileMiddleware`.
|
||||
var relativePath: String {
|
||||
let file = "\(fileName).\(fileExtension.rawValue)"
|
||||
|
||||
return subdirectory
|
||||
.map { "\($0)/\(file)" } ?? file
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Resolves the file's path against the given base directory.
|
||||
///
|
||||
/// - Parameter basePath: the directory the static files are served from.
|
||||
/// - Returns: the path to the file, relative to the `basePath` path.
|
||||
func path(
|
||||
relativeTo basePath: String
|
||||
) -> String {
|
||||
guard !basePath.isEmpty else {
|
||||
return relativePath
|
||||
}
|
||||
|
||||
return "\(basePath)/\(relativePath)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension StaticFile {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The sub-directory within the static root that holds the file, if any.
|
||||
/// The sub-directory within the static root that holds files with this extension, if any.
|
||||
var subdirectory: String? {
|
||||
switch self {
|
||||
case .appJS: "js"
|
||||
case .errorCSS,
|
||||
.styleCSS: "css"
|
||||
case .css: "css"
|
||||
case .js: "js"
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ struct ErrorPage: HTMLDocument, Sendable {
|
||||
|
||||
// MARK: Document
|
||||
|
||||
/// The page's content: a localized heading and explanatory message.
|
||||
/// The page's content: a localized heading and explanatory message, followed by the error and shared scripts.
|
||||
var body: some HTML {
|
||||
h1 {
|
||||
localize("error.heading", locale: locale)
|
||||
@@ -34,9 +34,11 @@ struct ErrorPage: HTMLDocument, Sendable {
|
||||
p {
|
||||
localize("error.message", locale: locale)
|
||||
}
|
||||
script(.src(StaticFile.error.urlPath(for: .js))) {}
|
||||
script(.src(StaticFile.shared.urlPath(for: .js))) {}
|
||||
}
|
||||
|
||||
/// The metadata and stylesheet link placed in the document head.
|
||||
/// The metadata and stylesheet links placed in the document head.
|
||||
var head: some HTML {
|
||||
meta(.charset(.utf8))
|
||||
meta(
|
||||
@@ -45,7 +47,11 @@ struct ErrorPage: HTMLDocument, Sendable {
|
||||
)
|
||||
link(
|
||||
.rel(.stylesheet),
|
||||
.href("/css/error.css")
|
||||
.href(StaticFile.shared.urlPath(for: .css))
|
||||
)
|
||||
link(
|
||||
.rel(.stylesheet),
|
||||
.href(StaticFile.error.urlPath(for: .css))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,13 @@ struct IndexPage: HTMLDocument, Sendable {
|
||||
|
||||
// MARK: Document
|
||||
|
||||
/// The page's content: a localized greeting followed by the app script.
|
||||
/// The page's content: a localized greeting followed by the shared and index scripts.
|
||||
var body: some HTML {
|
||||
p {
|
||||
localize("index.greeting", locale: locale)
|
||||
}
|
||||
script(.src("/js/app.js")) {}
|
||||
script(.src(StaticFile.index.urlPath(for: .js))) {}
|
||||
script(.src(StaticFile.shared.urlPath(for: .js))) {}
|
||||
}
|
||||
|
||||
/// The metadata, stylesheet, icon, and manifest links placed in the document head.
|
||||
@@ -43,11 +44,15 @@ struct IndexPage: HTMLDocument, Sendable {
|
||||
)
|
||||
link(
|
||||
.rel(.stylesheet),
|
||||
.href("/css/style.css")
|
||||
.href(StaticFile.shared.urlPath(for: .css))
|
||||
)
|
||||
link(
|
||||
.rel(.stylesheet),
|
||||
.href(StaticFile.index.urlPath(for: .css))
|
||||
)
|
||||
link(
|
||||
.rel(.icon),
|
||||
.href("/favicon.ico"),
|
||||
.href(StaticFile.favicon.urlPath(for: .ico)),
|
||||
.custom(
|
||||
name: "sizes",
|
||||
value: "any"
|
||||
@@ -55,7 +60,7 @@ struct IndexPage: HTMLDocument, Sendable {
|
||||
)
|
||||
link(
|
||||
.rel(.icon),
|
||||
.href("/icon.svg"),
|
||||
.href(StaticFile.icon.urlPath(for: .svg)),
|
||||
.custom(
|
||||
name: "type",
|
||||
value: "image/svg+xml"
|
||||
@@ -63,11 +68,11 @@ struct IndexPage: HTMLDocument, Sendable {
|
||||
)
|
||||
link(
|
||||
.rel("apple-touch-icon"),
|
||||
.href("/icon.png")
|
||||
.href(StaticFile.appleTouchIcon.urlPath(for: .png))
|
||||
)
|
||||
link(
|
||||
.rel("manifest"),
|
||||
.href("/site.webmanifest")
|
||||
.href(StaticFile.site.urlPath(for: .webmanifest))
|
||||
)
|
||||
meta(
|
||||
.name("theme-color"),
|
||||
|
||||
@@ -94,20 +94,22 @@ struct AppTests {
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/\(file.relativePath)",
|
||||
method: .get
|
||||
) { response in
|
||||
#expect(response.status == .ok)
|
||||
#expect(response.headers[.contentType] == file.contentType)
|
||||
|
||||
let cacheControl = try #require(response.headers[.cacheControl])
|
||||
for fileExtension in file.fileExtensions {
|
||||
try await client.execute(
|
||||
uri: "/\(file.relativePath(for: fileExtension))",
|
||||
method: .get
|
||||
) { response in
|
||||
#expect(response.status == .ok)
|
||||
#expect(response.headers[.contentType] == fileExtension.contentType)
|
||||
|
||||
#expect(cacheControl.contains("public") == true)
|
||||
#expect(cacheControl.contains("max-age=") == true)
|
||||
|
||||
if textExtensions.contains(file.fileExtension) {
|
||||
#expect(cacheControl.contains("must-revalidate") == true)
|
||||
let cacheControl = try #require(response.headers[.cacheControl])
|
||||
|
||||
#expect(cacheControl.contains("public") == true)
|
||||
#expect(cacheControl.contains("max-age=") == true)
|
||||
|
||||
if textExtensions.contains(fileExtension) {
|
||||
#expect(cacheControl.contains("must-revalidate") == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,25 @@ import Testing
|
||||
|
||||
@Suite("StaticFile enumeration")
|
||||
struct StaticFileTests {
|
||||
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
|
||||
typealias File = StaticFile
|
||||
typealias FileExtension = StaticFile.Extension
|
||||
|
||||
|
||||
// MARK: Computed tests
|
||||
|
||||
@Test(arguments: zip(
|
||||
File.allCases,
|
||||
Self.contentTypes
|
||||
))
|
||||
func `content type`(
|
||||
for file: File,
|
||||
expects contentType: String
|
||||
) {
|
||||
#expect(file.contentType == contentType)
|
||||
}
|
||||
|
||||
|
||||
@Test(arguments: zip(
|
||||
File.allCases,
|
||||
Self.fileExtensions
|
||||
))
|
||||
func `file extension`(
|
||||
func `file extensions`(
|
||||
for file: File,
|
||||
expects `extension`: FileExtension
|
||||
expects extensions: [FileExtension]
|
||||
) {
|
||||
#expect(file.fileExtension == `extension`)
|
||||
#expect(file.fileExtensions == extensions)
|
||||
}
|
||||
|
||||
|
||||
@Test(arguments: zip(
|
||||
File.allCases,
|
||||
Self.fileNames
|
||||
@@ -45,20 +34,57 @@ struct StaticFileTests {
|
||||
) {
|
||||
#expect(file.fileName == fileName)
|
||||
}
|
||||
|
||||
|
||||
@Test(arguments: zip(
|
||||
Self.extensions,
|
||||
Self.contentTypes
|
||||
))
|
||||
func `content type`(
|
||||
for fileExtension: FileExtension,
|
||||
expects contentType: String
|
||||
) {
|
||||
#expect(fileExtension.contentType == contentType)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Self.extensions,
|
||||
Self.subdirectories
|
||||
))
|
||||
func `subdirectory`(
|
||||
for fileExtension: FileExtension,
|
||||
expects subdirectory: String?
|
||||
) {
|
||||
#expect(fileExtension.subdirectory == subdirectory)
|
||||
}
|
||||
|
||||
// MARK: Method tests
|
||||
|
||||
@Test(arguments: zip(
|
||||
File.allCases,
|
||||
Self.relativePaths
|
||||
))
|
||||
func `relative path`(
|
||||
func `relative path for`(
|
||||
for file: File,
|
||||
expects relativePath: String
|
||||
expects relativePaths: [String]
|
||||
) {
|
||||
#expect(file.relativePath == relativePath)
|
||||
for (fileExtension, relativePath) in zip(file.fileExtensions, relativePaths) {
|
||||
#expect(file.relativePath(for: fileExtension) == relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Method tests
|
||||
|
||||
|
||||
@Test(arguments: zip(
|
||||
File.allCases,
|
||||
Self.relativePaths
|
||||
))
|
||||
func `url path for`(
|
||||
for file: File,
|
||||
expects relativePaths: [String]
|
||||
) {
|
||||
for (fileExtension, relativePath) in zip(file.fileExtensions, relativePaths) {
|
||||
#expect(file.urlPath(for: fileExtension) == "/\(relativePath)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
"",
|
||||
".",
|
||||
@@ -68,70 +94,99 @@ struct StaticFileTests {
|
||||
_ basePath: String
|
||||
) {
|
||||
for file in File.allCases {
|
||||
let pathRelativeToBasePath = file.path(relativeTo: basePath)
|
||||
for fileExtension in file.fileExtensions {
|
||||
let pathRelativeToBasePath = file.path(
|
||||
relativeTo: basePath,
|
||||
for: fileExtension
|
||||
)
|
||||
let relativePath = file.relativePath(for: fileExtension)
|
||||
|
||||
if basePath.isEmpty {
|
||||
#expect(pathRelativeToBasePath == file.relativePath)
|
||||
} else {
|
||||
#expect(pathRelativeToBasePath == "\(basePath)/\(file.relativePath)")
|
||||
if basePath.isEmpty {
|
||||
#expect(pathRelativeToBasePath == relativePath)
|
||||
} else {
|
||||
#expect(pathRelativeToBasePath == "\(basePath)/\(relativePath)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CaseIterable tests
|
||||
|
||||
|
||||
@Test
|
||||
func `all cases`() {
|
||||
#expect(File.allCases.count == 8)
|
||||
#expect(File.allCases.count == 9)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension StaticFileTests {
|
||||
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
static let contentTypes: [String] = [
|
||||
"text/javascript",
|
||||
"text/css",
|
||||
"image/vnd.microsoft.icon",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"text/plain",
|
||||
"application/manifest+json",
|
||||
"text/css"
|
||||
]
|
||||
static let fileExtensions: [FileExtension] = [
|
||||
.js,
|
||||
|
||||
static let extensions: [FileExtension] = [
|
||||
.css,
|
||||
.ico,
|
||||
.js,
|
||||
.png,
|
||||
.ico,
|
||||
.svg,
|
||||
.txt,
|
||||
.webmanifest,
|
||||
.css
|
||||
.xml
|
||||
]
|
||||
static let contentTypes: [String] = [
|
||||
"text/css",
|
||||
"text/javascript",
|
||||
"image/png",
|
||||
"image/vnd.microsoft.icon",
|
||||
"image/svg+xml",
|
||||
"text/plain",
|
||||
"application/manifest+json",
|
||||
"application/xml"
|
||||
]
|
||||
static let subdirectories: [String?] = [
|
||||
"css",
|
||||
"js",
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil
|
||||
]
|
||||
static let fileExtensions: [[FileExtension]] = [
|
||||
[.png],
|
||||
[.css, .js],
|
||||
[.ico],
|
||||
[.png, .svg],
|
||||
[.css, .js],
|
||||
[.txt],
|
||||
[.css, .js],
|
||||
[.webmanifest],
|
||||
[.xml]
|
||||
]
|
||||
static let fileNames: [String] = [
|
||||
"app",
|
||||
"apple-touch-icon",
|
||||
"error",
|
||||
"favicon",
|
||||
"icon",
|
||||
"icon",
|
||||
"index",
|
||||
"robots",
|
||||
"shared",
|
||||
"site",
|
||||
"style"
|
||||
"sitemap"
|
||||
]
|
||||
static let relativePaths: [String] = [
|
||||
"js/app.js",
|
||||
"css/error.css",
|
||||
"favicon.ico",
|
||||
"icon.png",
|
||||
"icon.svg",
|
||||
"robots.txt",
|
||||
"site.webmanifest",
|
||||
"css/style.css"
|
||||
static let relativePaths: [[String]] = [
|
||||
["apple-touch-icon.png"],
|
||||
["css/error.css", "js/error.js"],
|
||||
["favicon.ico"],
|
||||
["icon.png", "icon.svg"],
|
||||
["css/index.css", "js/index.js"],
|
||||
["robots.txt"],
|
||||
["css/shared.css", "js/shared.js"],
|
||||
["site.webmanifest"],
|
||||
["sitemap.xml"]
|
||||
]
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ struct ErrorPageTests {
|
||||
#expect(html.contains(#"lang="en""#))
|
||||
#expect(html.contains("Page Not Found"))
|
||||
#expect(html.contains("Sorry, but the page you were trying to view does not exist."))
|
||||
#expect(html.contains("/css/shared.css"))
|
||||
#expect(html.contains("/css/error.css"))
|
||||
#expect(html.contains("/js/error.js"))
|
||||
#expect(html.contains("/js/shared.js"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,13 +17,15 @@ struct IndexPageTests {
|
||||
|
||||
#expect(html.contains("<!DOCTYPE html>"))
|
||||
#expect(html.contains(#"lang="en""#))
|
||||
#expect(html.contains("/css/style.css"))
|
||||
#expect(html.contains("/css/shared.css"))
|
||||
#expect(html.contains("/css/index.css"))
|
||||
#expect(html.contains("/favicon.ico"))
|
||||
#expect(html.contains("/icon.svg"))
|
||||
#expect(html.contains("/icon.png"))
|
||||
#expect(html.contains("/apple-touch-icon.png"))
|
||||
#expect(html.contains("/site.webmanifest"))
|
||||
#expect(html.contains("Hello world!"))
|
||||
#expect(html.contains("/js/app.js"))
|
||||
#expect(html.contains("/js/shared.js"))
|
||||
#expect(html.contains("/js/index.js"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user