diff --git a/Makefile b/Makefile index f4a99f0d0..5730352fd 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ help: echo ' check-php - check that the modified PHP files are syntactically correct' echo ' composer-dev-update - run local composer update' echo ' composer-live-update - run production composer install from composer.lock' + echo ' config-css - generate the configuration variables to build the CSS files' echo ' dump-all - create tarballs of the following:' echo ' dump-riplog - create a tarball of the rip logs' echo ' dump-riploghtml - create a tarball of the HTMLified rip logs' @@ -35,6 +36,7 @@ help: .PHONY: build-css build-css: + docker compose exec -T web bin/config-css /tmp/config-css.js docker compose exec -T web npm run build:scss .PHONY: check-php diff --git a/bin/build-scss.mjs b/bin/build-scss.mjs index 3b177a3f0..eaccbbca6 100644 --- a/bin/build-scss.mjs +++ b/bin/build-scss.mjs @@ -1,20 +1,43 @@ +import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import * as sass from 'sass'; const isProduction = process.env.NODE_ENV === 'production'; -const style = isProduction ? 'compressed' : 'expanded'; + +const style = isProduction ? 'compressed' : 'expanded'; const sourceMap = isProduction; - const __dirname = import.meta.dirname; +const rootDir = path.join(__dirname, '..'); +const sassDir = path.join(rootDir, 'sass'); +const stylesDir = path.join(rootDir, 'public', 'static', 'styles'); -const sassDir = path.join(__dirname, '..', 'sass'); -const stylesDir = path.join(__dirname, '..', 'public', 'static', 'styles'); +if (process.argv.length < 3) { + console.error('config filename not specified'); + process.exit(1); +} + +const loadJSON = (path) => JSON.parse(fs.readFileSync(new URL(path, import.meta.url))); +const config = loadJSON(process.argv[2]); const skipItems = [ 'opendyslexic', ]; +const sassOptions = { + functions: { + 'config($key)': (args) => { + const key = args[0].assertString('key').toString().replace(/^['"]+|['"]+$/g, ''); + if (!config[key]) { + throw new Error(`Unknown config key: ${key}`); + } + return new sass.SassString(config[key]); + }, + }, + sourceMap, + style, +} + for (const item of fs.readdirSync(sassDir, { withFileTypes: true })) { let sourceFile; let outputFile; @@ -35,7 +58,7 @@ for (const item of fs.readdirSync(sassDir, { withFileTypes: true })) { outputFile = path.join(stylesDir, item.name.replace('.scss', '.css')); outputSourcemap = path.join(stylesDir, item.name.replace('.scss', '.css.map')); } - const result = sass.compile(sourceFile, { sourceMap, style }); + const result = sass.compile(sourceFile, sassOptions); fs.writeFileSync(outputFile, result.css); if (result.sourceMap) { fs.writeFileSync( diff --git a/bin/config-css b/bin/config-css new file mode 100755 index 000000000..cde7d722f --- /dev/null +++ b/bin/config-css @@ -0,0 +1,21 @@ +#!/usr/bin/env php + SITE_HOST, + 'SITE_NAME' => SITE_NAME, + 'SITE_URL' => SITE_URL, + 'STATIC_SERVER' => STATIC_SERVER, + ]) . "\n" +); +fclose($out); diff --git a/lib/config.php b/lib/config.php index 78cd3579a..aa5f5646f 100644 --- a/lib/config.php +++ b/lib/config.php @@ -71,7 +71,7 @@ defined('STORAGE_PATH_RIPLOGHTML') or define('STORAGE_PATH_RIPLOGHTML', '/var/li // Host static assets (images, css, js) on another server. // In development it is just a folder -defined('STATIC_SERVER') or define('STATIC_SERVER', 'static'); +defined('STATIC_SERVER') or define('STATIC_SERVER', '/static'); // Where is the repository physically stored (and hence where the document // root lives). This is needed so that Gazelle knows where static assets are, diff --git a/misc/docker/web/bootstrap-npm.sh b/misc/docker/web/bootstrap-npm.sh index efaabe65d..028485b0f 100755 --- a/misc/docker/web/bootstrap-npm.sh +++ b/misc/docker/web/bootstrap-npm.sh @@ -5,6 +5,8 @@ set -euo pipefail npm_config_cache="${CI_PROJECT_DIR}/node_modules/.npm-cache" export npm_config_cache +bin/config-css /tmp/config-css.js + npm install npx update-browserslist-db@latest npx puppeteer browsers install chrome diff --git a/package.json b/package.json index 82d6e4aae..868a0ded3 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "npm run build", "prod": "cross-env NODE_ENV=production npm run build", "build": "npm run build:scss && npm run build:previews", - "build:scss": "node bin/build-scss.mjs", + "build:scss": "node bin/build-scss.mjs /tmp/config-css.js", "build:previews": "node bin/build-stylesheet-gallery.mjs", "start": "npm run build:scss -- --watch" }, diff --git a/sass/apollostage/_base.scss b/sass/apollostage/_base.scss index 2c657ee17..ab9891bb8 100644 --- a/sass/apollostage/_base.scss +++ b/sass/apollostage/_base.scss @@ -2437,7 +2437,7 @@ tr.snatched_torrent div.tags::before { border: thick solid variables.$color-accent-border-warning-2; padding: 1rem 1rem 1rem 48px; background-color: variables.$color-background-warning; - background-image: url("/svg/site/ops-icon-triangle-ccc.svg"); + background-image: url(config('STATIC_SERVER') + "/svg/site/ops-icon-triangle-ccc.svg"); background-repeat: no-repeat; background-position: 8px; background-size: 32px; diff --git a/sass/apollostage/_variables.scss b/sass/apollostage/_variables.scss index 92398dfc4..385f2275d 100644 --- a/sass/apollostage/_variables.scss +++ b/sass/apollostage/_variables.scss @@ -234,41 +234,41 @@ $color-anchor-warning: $fore-1000; // Forum icons -$icon-message-unread: url("/svg/std/document-round-ccc.svg"); -$icon-message-unread-locked: url("/svg/std/lock-round-ccc.svg"); -$icon-message-unread-sticky: url("/svg/std/pin-round-ccc.svg"); -$icon-message-unread-locked-sticky: url("/svg/std/pin-and-lock-round-ccc.svg"); -$icon-message-read: url("/svg/std/document-ccc.svg"); -$icon-message-read-locked: url("/svg/std/lock-ccc.svg"); -$icon-message-read-sticky: url("/svg/std/pin-ccc.svg"); -$icon-message-read-locked-sticky: url("/svg/std/pin-and-lock-ccc.svg"); +$icon-message-unread: url(config('STATIC_SERVER') + "/svg/std/document-round-ccc.svg"); +$icon-message-unread-locked: url(config('STATIC_SERVER') + "/svg/std/lock-round-ccc.svg"); +$icon-message-unread-sticky: url(config('STATIC_SERVER') + "/svg/std/pin-round-ccc.svg"); +$icon-message-unread-locked-sticky: url(config('STATIC_SERVER') + "/svg/std/pin-and-lock-round-ccc.svg"); +$icon-message-read: url(config('STATIC_SERVER') + "/svg/std/document-ccc.svg"); +$icon-message-read-locked: url(config('STATIC_SERVER') + "/svg/std/lock-ccc.svg"); +$icon-message-read-sticky: url(config('STATIC_SERVER') + "/svg/std/pin-ccc.svg"); +$icon-message-read-locked-sticky: url(config('STATIC_SERVER') + "/svg/std/pin-and-lock-ccc.svg"); // Header Pulldown Menu (hamburger/chevron icon) -$icon-menu-open-state: url("/svg/std/menu-ccc.svg"); -$icon-menu-closed-state: url("/svg/std/chevrons-down-ccc.svg"); -$icon-menu-pulldown-mobile: url("/svg/std/chevrons-down-ccc.svg"); +$icon-menu-open-state: url(config('STATIC_SERVER') + "/svg/std/menu-ccc.svg"); +$icon-menu-closed-state: url(config('STATIC_SERVER') + "/svg/std/chevrons-down-ccc.svg"); +$icon-menu-pulldown-mobile: url(config('STATIC_SERVER') + "/svg/std/chevrons-down-ccc.svg"); // Used in Header // Orpheus specific. Other deployments, provide your own. -$logo-site-lockup: url("/svg/site/ops-logo-lockup-darkmode.svg"); -$logo-site-square: url("/svg/site/ops-logo-loop-darkmode.svg"); +$logo-site-lockup: url(config('STATIC_SERVER') + "/svg/site/ops-logo-lockup-darkmode.svg"); +$logo-site-square: url(config('STATIC_SERVER') + "/svg/site/ops-logo-loop-darkmode.svg"); // Used on Mobile -$icon-number-snatches: url("/svg/std/arrow-loop-ccc.svg"); -$icon-number-seeders: url("/svg/std/arrow-up-round-ccc.svg"); -$icon-number-leechers: url("/svg/std/arrow-down-round-ccc.svg"); +$icon-number-snatches: url(config('STATIC_SERVER') + "/svg/std/arrow-loop-ccc.svg"); +$icon-number-seeders: url(config('STATIC_SERVER') + "/svg/std/arrow-up-round-ccc.svg"); +$icon-number-leechers: url(config('STATIC_SERVER') + "/svg/std/arrow-down-round-ccc.svg"); // Used on alerts // // standard: -$icon-mobile-rotate: url("/svg/std/notify-rotate-darkmode-ccc.svg"); +$icon-mobile-rotate: url(config('STATIC_SERVER') + "/svg/std/notify-rotate-darkmode-ccc.svg"); // and -// $icon-notification: url("/svg/std/notification-bell-333xccc.svg"); -// $icon-warning: url("/svg/std/x-c33.svg") +// $icon-notification: url(config('STATIC_SERVER') + "/svg/std/notification-bell-333xccc.svg"); +// $icon-warning: url(config('STATIC_SERVER') + "/svg/std/x-c33.svg") // // Orpheus specific. Other deployments, use the above. -$icon-notification: url("/svg/site/ops-icon-harp-ccc.svg"); -$icon-warning: url("/svg/site/ops-icon-triangle-000.svg"); +$icon-notification: url(config('STATIC_SERVER') + "/svg/site/ops-icon-harp-ccc.svg"); +$icon-warning: url(config('STATIC_SERVER') + "/svg/site/ops-icon-triangle-000.svg"); // EOF // //////////////////////////////////////////////////////////////////////////////// diff --git a/sass/apollostage_coffee/style.scss b/sass/apollostage_coffee/style.scss index b0b5a32be..be59d73b0 100644 --- a/sass/apollostage_coffee/style.scss +++ b/sass/apollostage_coffee/style.scss @@ -195,7 +195,7 @@ variables.$color-anchor-alert: variables.$fore-1000; variables.$ds-shadow-heavy: 0 10px 18px rgb(0 0 0 / 55%), 0 7px 7px rgb(0 0 0 / 52%), 0 -5px 9px rgb(0 0 0 / 32%); variables.$ds-shadow-raised: 0 10px 18px rgb(0 0 0 / 25%), 0 7px 7px rgb(0 0 0 / 22%); variables.$ds-shadow-raised-hover: 0 14px 28px rgb(0 0 0 / 25%), 0 10px 10px rgb(0 0 0 / 22%); -variables.$icon-menu-open-state: url("/svg/std/menu-ccc.svg"); // change to lighter version +variables.$icon-menu-open-state: url(config('STATIC_SERVER') + "/svg/std/menu-ccc.svg"); // change to lighter version @use '../apollostage/_base'; diff --git a/sass/apollostage_sunset/style.scss b/sass/apollostage_sunset/style.scss index c94527a3e..d3d25e14b 100644 --- a/sass/apollostage_sunset/style.scss +++ b/sass/apollostage_sunset/style.scss @@ -203,7 +203,7 @@ variables.$ds-shadow-raised: 0 10px 18px rgb(0 0 0 / 25%), 0 7px 7px rgb(0 0 0 / variables.$ds-shadow-raised-hover: 0 14px 28px rgb(0 0 0 / 25%), 0 10px 10px rgb(0 0 0 / 22%); // icon -variables.$icon-mobile-rotate: url("/svg/std/notify-rotate-lightmode.svg"); //flip to lightmode +variables.$icon-mobile-rotate: url(config('STATIC_SERVER') + "/svg/std/notify-rotate-lightmode.svg"); //flip to lightmode @use '../apollostage/_base'; diff --git a/sass/global.scss b/sass/global.scss index 6a2979075..11d84b48c 100644 --- a/sass/global.scss +++ b/sass/global.scss @@ -964,7 +964,7 @@ input[type="search"] { border-radius: 0.25rem; padding: 1rem 1rem 1rem 48px; background-color: #eaebeb; - background-image: url("/svg/site/ops-icon-triangle-000.svg"); + background-image: url(config('STATIC_SERVER') + "/svg/site/ops-icon-triangle-000.svg"); background-repeat: no-repeat; background-position: 8px 1rem; background-size: 32px; diff --git a/sass/orpheus_paper/style.scss b/sass/orpheus_paper/style.scss index dbbee5e4a..48ada772f 100644 --- a/sass/orpheus_paper/style.scss +++ b/sass/orpheus_paper/style.scss @@ -247,14 +247,14 @@ variables.$color-anchor-alert: variables.$ds-gray-05; // ICONS // changing to #000 black version -variables.$icon-message-unread: url("/svg/std/document-round-000.svg"); -variables.$icon-message-unread-locked: url("/svg/std/lock-round-000.svg"); -variables.$icon-message-unread-sticky: url("/svg/std/pin-round-000.svg"); -variables.$icon-message-unread-locked-sticky: url("/svg/std/pin-and-lock-round-000.svg"); -variables.$icon-message-read: url("/svg/std/document-000.svg"); -variables.$icon-message-read-locked: url("/svg/std/lock-000.svg"); -variables.$icon-message-read-sticky: url("/svg/std/pin-000.svg"); -variables.$icon-message-read-locked-sticky: url("/svg/std/pin-and-lock-000.svg"); +variables.$icon-message-unread: url(config('STATIC_SERVER') + "/svg/std/document-round-000.svg"); +variables.$icon-message-unread-locked: url(config('STATIC_SERVER') + "/svg/std/lock-round-000.svg"); +variables.$icon-message-unread-sticky: url(config('STATIC_SERVER') + "/svg/std/pin-round-000.svg"); +variables.$icon-message-unread-locked-sticky: url(config('STATIC_SERVER') + "/svg/std/pin-and-lock-round-000.svg"); +variables.$icon-message-read: url(config('STATIC_SERVER') + "/svg/std/document-000.svg"); +variables.$icon-message-read-locked: url(config('STATIC_SERVER') + "/svg/std/lock-000.svg"); +variables.$icon-message-read-sticky: url(config('STATIC_SERVER') + "/svg/std/pin-000.svg"); +variables.$icon-message-read-locked-sticky: url(config('STATIC_SERVER') + "/svg/std/pin-and-lock-000.svg"); // Header Pulldown Menu (hamburger/chevron icon) // not changing, still dark background @@ -262,21 +262,21 @@ variables.$icon-message-read-locked-sticky: url("/svg/std/pin-and-lock-000.svg") // Used in Header // changing to light mode version // // Orpheus specific: -variables.$logo-site-lockup: url("/svg/site/ops-logo-lockup-lightmode.svg"); -variables.$logo-site-square: url("/svg/site/ops-logo-loop-lightmode.svg"); +variables.$logo-site-lockup: url(config('STATIC_SERVER') + "/svg/site/ops-logo-lockup-lightmode.svg"); +variables.$logo-site-square: url(config('STATIC_SERVER') + "/svg/site/ops-logo-loop-lightmode.svg"); // Used on Mobile // changing to #000 black version -variables.$icon-number-snatches: url("/svg/std/arrow-loop-000.svg"); -variables.$icon-number-seeders: url("/svg/std/arrow-up-round-000.svg"); -variables.$icon-number-leechers: url("/svg/std/arrow-down-round-000.svg"); +variables.$icon-number-snatches: url(config('STATIC_SERVER') + "/svg/std/arrow-loop-000.svg"); +variables.$icon-number-seeders: url(config('STATIC_SERVER') + "/svg/std/arrow-up-round-000.svg"); +variables.$icon-number-leechers: url(config('STATIC_SERVER') + "/svg/std/arrow-down-round-000.svg"); // // standard: -// $icon-notification: url("/svg/std/notification-bell-000.svg"); -// $icon-warning: url("/svg/std/x-000.svg") +// $icon-notification: url(config('STATIC_SERVER') + "/svg/std/notification-bell-000.svg"); +// $icon-warning: url(config('STATIC_SERVER') + "/svg/std/x-000.svg") // // Orpheus specific; other deployments, use the above. -variables.$icon-notification: url("/svg/site/ops-icon-harp-000.svg"); -variables.$icon-warning: url("/svg/site/ops-icon-triangle-000.svg"); +variables.$icon-notification: url(config('STATIC_SERVER') + "/svg/site/ops-icon-harp-000.svg"); +variables.$icon-warning: url(config('STATIC_SERVER') + "/svg/site/ops-icon-triangle-000.svg"); @use '../apollostage/_base'; diff --git a/tests/phpunit/DonorTest.php b/tests/phpunit/DonorTest.php index 26034a85b..2bdcc53be 100644 --- a/tests/phpunit/DonorTest.php +++ b/tests/phpunit/DonorTest.php @@ -84,7 +84,7 @@ class DonorTest extends TestCase { $this->assertEquals('1 [Red]', $donor->rankLabel(), 'donor-rank-label-1'); $this->assertEquals(1, $donor->collageTotal(), 'donor-collage-1'); $this->assertEquals('Donor', $donor->iconHoverText(), 'donor-icon-hover-text-1'); - $this->assertEquals('static/common/symbols/donor.png', $donor->heartIcon(), 'donor-heart-icon-1'); + $this->assertEquals('/static/common/symbols/donor.png', $donor->heartIcon(), 'donor-heart-icon-1'); $this->assertGreaterThan(0, $donor->leaderboardRank(), 'donor-leaderboard-rank-1'); $this->assertFalse($donor->avatarHover(), 'donor-avatar-hover-1'); @@ -166,7 +166,7 @@ class DonorTest extends TestCase { $this->assertTrue($donor->hasMaxSpecialRank(), 'donor-mod-has-max-special'); $this->assertEquals('Never', $donor->rankExpiry(), 'donor-mod-rank-expiry'); $this->assertEquals('∞ [Diamond]', $donor->rankLabel(), 'donor-mod-rank-label'); - $this->assertEquals('static/common/symbols/donor_6.png', $donor->heartIcon(), 'donor-mod-heart-icon'); + $this->assertEquals('/static/common/symbols/donor_6.png', $donor->heartIcon(), 'donor-mod-heart-icon'); $this->assertEquals('', $donor->profileInfo(1), 'donor-mod-profile1-info'); $this->assertEquals('', $donor->profileTitle(1), 'donor-mod-profile1-title'); $this->assertEquals('', $donor->profileInfo(2), 'donor-mod-profile2-info'); @@ -202,7 +202,7 @@ class DonorTest extends TestCase { $this->assertEquals('donate.php', $donor->iconLink(), 'donor-icon-link-2'); $this->assertEquals('donate.php', $donor->iconLink(), 'donor-icon-link-2'); $this->assertEquals('Donor', $donor->iconHoverText(), 'donor-icon-hover-text-2'); - $this->assertEquals('static/common/symbols/donor_2.png', $donor->heartIcon(), 'donor-heart-icon-2'); + $this->assertEquals('/static/common/symbols/donor_2.png', $donor->heartIcon(), 'donor-heart-icon-2'); $this->assertTrue($donor->hasForum(), 'donor-has-forum-2'); $this->assertTrue($donor->hasRankAbove(1), 'donor-has-above-rank1-2'); @@ -233,7 +233,7 @@ class DonorTest extends TestCase { $this->assertEquals(3, $donor->collageTotal(), 'donor-collage-3'); $this->assertEquals('3 [Bronze]', $donor->rankLabel(), 'donor-rank-label-2'); $this->assertEquals(4, $donor->invitesReceived(), 'donor-received-3-for-4-invites'); - $this->assertEquals('static/common/symbols/donor_3.png', $donor->heartIcon(), 'donor-heart-icon-3'); + $this->assertEquals('/static/common/symbols/donor_3.png', $donor->heartIcon(), 'donor-heart-icon-3'); $this->assertTrue($donor->updateProfileInfo(2, 'phpunit donor info 2')->modify(), 'donor-profile2-info-2'); $this->assertEquals('', $donor->avatarHoverText(), 'donor-no-avatar-hover-text-2'); $this->assertTrue($donor->updateAvatarHoverText('avatar hover')->modify(), 'donor-update-avatar-hover-2'); @@ -254,7 +254,7 @@ class DonorTest extends TestCase { $this->assertEquals(0, $donor->specialRank(), 'donor-not-special-rank-4'); $this->assertEquals(4, $donor->collageTotal(), 'donor-collage-4'); $this->assertEquals('4 [Silver]', $donor->rankLabel(), 'donor-rank-label-2'); - $this->assertEquals('static/common/symbols/donor_4.png', $donor->heartIcon(), 'donor-heart-icon-4'); + $this->assertEquals('/static/common/symbols/donor_4.png', $donor->heartIcon(), 'donor-heart-icon-4'); $this->assertTrue($donor->updateProfileInfo(3, 'phpunit donor info 3')->modify(), 'donor-profile3-info-2'); $this->assertTrue($donor->updateIconLink('https://example.com/')->modify(), 'donor-update-icon-link-2'); $this->assertEquals('https://example.com/', $donor->iconLink(), 'donor-icon-link-2'); @@ -274,7 +274,7 @@ class DonorTest extends TestCase { $this->assertEquals(0, $donor->specialRank(), 'donor-not-special-rank-5'); $this->assertEquals(5, $donor->collageTotal(), 'donor-collage-5'); $this->assertEquals('4 [Silver]', $donor->rankLabel(), 'donor-rank-label-4'); - $this->assertEquals('static/common/symbols/donor_4.png', $donor->heartIcon(), 'donor-heart-icon-4'); + $this->assertEquals('/static/common/symbols/donor_4.png', $donor->heartIcon(), 'donor-heart-icon-4'); $this->assertFalse($donor->forumUseComma(), 'donor-has-forum-comma'); $this->assertTrue($donor->setForumDecoration('The', 'Person', false), 'donor-set-forum-decoration'); @@ -302,7 +302,7 @@ class DonorTest extends TestCase { $this->assertEquals(0, $donor->specialRank(), 'donor-not-special-rank-5'); $this->assertEquals(5, $donor->collageTotal(), 'donor-collage-6-is-5'); $this->assertEquals('5 [Gold]', $donor->rankLabel(), 'donor-rank-label-5'); - $this->assertEquals('static/common/symbols/donor_5.png', $donor->heartIcon(), 'donor-heart-icon-5'); + $this->assertEquals('/static/common/symbols/donor_5.png', $donor->heartIcon(), 'donor-heart-icon-5'); $this->assertCount(5, $donor->historyList(), 'donor-history-list'); $this->assertFalse($donor->hasDonorPick(), 'donor-has-no-donor-pick'); $this->assertFalse($donor->avatarHover(), 'donor-has-no-avatar-hover'); diff --git a/tests/phpunit/UserCreateTest.php b/tests/phpunit/UserCreateTest.php index 67dfc060c..4f2f3093d 100644 --- a/tests/phpunit/UserCreateTest.php +++ b/tests/phpunit/UserCreateTest.php @@ -31,7 +31,7 @@ class UserCreateTest extends TestCase { $this->assertStringContainsString($adminComment, $this->user->staffNotes(), 'user-create-staff-notes'); $this->assertTrue($this->user->isUnconfirmed(), 'user-create-unconfirmed'); $this->assertStringStartsWith( - 'static/styles/apollostage/style.css?v=', + '/static/styles/apollostage/style.css?v=', (new User\Stylesheet($this->user))->cssUrl(), 'user-create-stylesheet' ); diff --git a/tests/phpunit/UserTest.php b/tests/phpunit/UserTest.php index 89d738dc5..9c7427da2 100644 --- a/tests/phpunit/UserTest.php +++ b/tests/phpunit/UserTest.php @@ -324,13 +324,13 @@ class UserTest extends TestCase { $this->assertEquals(count($list), count($manager->usageList('name', 'ASC')), 'stylesheet-list-usage'); $first = current($list); - $url = SITE_URL . 'static/bogus.css'; + $url = STATIC_SERVER . '/bogus.css'; $stylesheet = new User\Stylesheet($this->user); $this->assertNull($stylesheet->styleUrl(), 'stylesheet-no-external-url'); $this->assertEquals(1, $stylesheet->modifyInfo($first['id'], null), 'stylesheet-modify'); $this->assertEquals($first['css_name'], $stylesheet->cssName(), 'stylesheet-css-name'); - $this->assertStringStartsWith("static/styles/{$first['css_name']}/style.css?v=", $stylesheet->cssUrl(), 'stylesheet-css-url'); - $this->assertEquals("static/styles/{$first['css_name']}/images/", $stylesheet->imagePath(), 'stylesheet-image-path'); + $this->assertStringStartsWith("/static/styles/{$first['css_name']}/style.css?v=", $stylesheet->cssUrl(), 'stylesheet-css-url'); + $this->assertEquals("/static/styles/{$first['css_name']}/images/", $stylesheet->imagePath(), 'stylesheet-image-path'); $this->assertEquals($first['name'], $stylesheet->name(), 'stylesheet-name'); $this->assertEquals($first['id'], $stylesheet->styleId(), 'stylesheet-style-id'); $this->assertEquals($first['theme'], $stylesheet->theme(), 'stylesheet-theme');