mirror of
https://github.com/ratspeak/Ratspeak
synced 2026-08-12 18:07:35 -04:00
109 lines
4.9 KiB
JavaScript
109 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
var assert = require('assert');
|
|
var childProcess = require('child_process');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
|
|
var dashboardRoot = path.join(__dirname, '..');
|
|
var cssRoot = path.join(dashboardRoot, 'static', 'css');
|
|
var modules = [
|
|
'00-tokens.css',
|
|
'01-reset.css',
|
|
'02-typography.css',
|
|
'03-scrollbar.css',
|
|
'04-layout.css',
|
|
'05-panels.css',
|
|
'06-forms.css',
|
|
'07-components.css',
|
|
'08-modals.css',
|
|
'09-messaging.css',
|
|
'09-channels.css',
|
|
'10-views.css',
|
|
'11-games.css',
|
|
'12-animations.css',
|
|
'13-responsive.css'
|
|
];
|
|
|
|
var sources = modules.map(function(name) {
|
|
return {
|
|
name: name,
|
|
text: fs.readFileSync(path.join(cssRoot, name), 'utf8')
|
|
};
|
|
});
|
|
var expectedBundle = sources.map(function(source) { return source.text + '\n'; }).join('');
|
|
var shellBuilder = fs.readFileSync(path.join(dashboardRoot, 'build-css.sh'), 'utf8');
|
|
var shellModules = Array.from(shellBuilder.matchAll(/^\s+([0-9][^\s]+\.css)\s*$/gm), function(match) { return match[1]; });
|
|
assert.deepStrictEqual(shellModules, modules, 'the shell CSS builder must use the canonical module order');
|
|
assert(!/\bsed\b|\bminif/i.test(shellBuilder), 'the shell CSS builder must not rewrite only its output');
|
|
|
|
var rustBuilder = fs.readFileSync(path.join(dashboardRoot, '..', 'src-tauri', 'build.rs'), 'utf8');
|
|
var rustModules = Array.from(rustBuilder.matchAll(/"([0-9][^"]+\.css)"/g), function(match) { return match[1]; });
|
|
assert.deepStrictEqual(rustModules, modules, 'the Cargo CSS builder must use the canonical module order');
|
|
|
|
if (process.platform !== 'win32') {
|
|
childProcess.execFileSync(path.join(dashboardRoot, 'build-css.sh'), [], {
|
|
cwd: path.dirname(dashboardRoot),
|
|
stdio: 'pipe'
|
|
});
|
|
}
|
|
var bundlePath = path.join(dashboardRoot, 'static', 'style.css');
|
|
var actualBundle = fs.existsSync(bundlePath) ? fs.readFileSync(bundlePath, 'utf8') : expectedBundle;
|
|
assert.strictEqual(actualBundle, expectedBundle,
|
|
'static/style.css must be the exact deterministic module concatenation');
|
|
|
|
var allCss = sources.map(function(source) { return source.text; }).join('\n');
|
|
var defined = new Set();
|
|
var definitionRe = /(--[a-zA-Z0-9_-]+)\s*:/g;
|
|
var match;
|
|
while ((match = definitionRe.exec(allCss))) defined.add(match[1]);
|
|
|
|
var missing = [];
|
|
sources.forEach(function(source) {
|
|
var referenceRe = /var\(\s*(--[a-zA-Z0-9_-]+)\s*([^)]*)\)/g;
|
|
var ref;
|
|
while ((ref = referenceRe.exec(source.text))) {
|
|
if (/^\s*,/.test(ref[2])) continue;
|
|
if (!defined.has(ref[1])) missing.push(source.name + ': ' + ref[1]);
|
|
}
|
|
});
|
|
assert.deepStrictEqual(missing, [], 'every no-fallback CSS variable reference must be defined');
|
|
|
|
var keyframes = new Map();
|
|
sources.forEach(function(source) {
|
|
var keyframeRe = /@keyframes\s+([a-zA-Z0-9_-]+)/g;
|
|
var frame;
|
|
while ((frame = keyframeRe.exec(source.text))) {
|
|
var owners = keyframes.get(frame[1]) || [];
|
|
owners.push(source.name);
|
|
keyframes.set(frame[1], owners);
|
|
}
|
|
});
|
|
var duplicates = [];
|
|
keyframes.forEach(function(owners, name) {
|
|
if (owners.length > 1) duplicates.push(name + ': ' + owners.join(', '));
|
|
});
|
|
assert.deepStrictEqual(duplicates, [], 'animation keyframes must have one owner');
|
|
|
|
var tokens = sources[0].text;
|
|
assert(/--text-base:\s*0\.9375rem/.test(tokens), 'text tokens must scale from rem units');
|
|
assert(/--text-3xl:\s*1\.75rem/.test(tokens), 'the complete text scale must be defined');
|
|
assert(!/font-weight:\s*800\b/.test(allCss), 'CSS must not request an unloaded Outfit weight');
|
|
assert(!/--font-sans\s*:/.test(sources[10].text), 'Channels must inherit the app font contract');
|
|
assert(/\.nav-item\s*\{[^}]*min-height:\s*46px/s.test(sources[4].text),
|
|
'navigation labels must grow instead of clipping scaled text');
|
|
var settingsFieldRule = sources[6].text.match(/\.view-grid-settings input\[type="text"\][\s\S]*?\.view-grid-settings \.modal-input\s*\{([^}]*)\}/);
|
|
assert(settingsFieldRule && /min-height:\s*34px/.test(settingsFieldRule[1]) && /height:\s*auto/.test(settingsFieldRule[1]),
|
|
'settings fields must grow with scaled text');
|
|
assert(/data-text-scale-tier="xlarge"[\s\S]*?\.channels-layout/.test(sources[14].text),
|
|
'the largest text tier must simplify the Channels layout');
|
|
assert(/\.settings-type-presets\s*\{[\s\S]*?repeat\(5,/.test(sources[11].text),
|
|
'text sizing must expose five deliberate presets instead of a continuous slider');
|
|
assert(/data-scale="100"[\s\S]*?--type-preview-size:\s*18px/.test(sources[11].text) &&
|
|
/data-scale="140"[\s\S]*?--type-preview-size:\s*34px/.test(sources[11].text),
|
|
'text-size specimens must make the preset progression visually distinct');
|
|
assert(/data-text-scale-tier="large"\] \.settings-text-scale-row \.settings-row-info\s*\{[^}]*flex:\s*0 0 auto/s.test(sources[11].text),
|
|
'the 130% tier must not stretch the text-size introduction away from its presets');
|
|
|
|
console.log('CSS contract tests passed');
|