add biome linting and formatting for a consistent style and easier contributions

this should prevent useless conflicts
This commit is contained in:
lovasoa
2024-11-05 21:03:20 +01:00
parent 4623535532
commit 6081c9ff9f
26 changed files with 5849 additions and 4961 deletions
+62 -81
View File
@@ -1,83 +1,64 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'sqlpage'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=sqlpage"
],
"filter": {
"name": "sqlpage",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'sqlpage'",
"cargo": {
"args": [
"build",
"--bin=sqlpage",
"--package=sqlpage"
],
"filter": {
"name": "sqlpage",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'sqlpage'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=sqlpage",
"--package=sqlpage"
],
"filter": {
"name": "sqlpage",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 'index'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=index",
"--package=sqlpage"
],
"filter": {
"name": "index",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'sqlpage'",
"cargo": {
"args": ["test", "--no-run", "--lib", "--package=sqlpage"],
"filter": {
"name": "sqlpage",
"kind": "lib"
}
]
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'sqlpage'",
"cargo": {
"args": ["build", "--bin=sqlpage", "--package=sqlpage"],
"filter": {
"name": "sqlpage",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'sqlpage'",
"cargo": {
"args": ["test", "--no-run", "--bin=sqlpage", "--package=sqlpage"],
"filter": {
"name": "sqlpage",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 'index'",
"cargo": {
"args": ["test", "--no-run", "--test=index", "--package=sqlpage"],
"filter": {
"name": "index",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
+2 -4
View File
@@ -1,5 +1,3 @@
{
"rust-analyzer.linkedProjects": [
"./Cargo.toml"
]
}
"rust-analyzer.linkedProjects": ["./Cargo.toml"]
}
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"files": {
"ignore": ["examples/official-site/pgconf/**"],
"ignoreUnknown": true
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2
},
"vcs": {
"enabled": true,
"useIgnoreFile": true,
"clientKind": "git"
}
}
@@ -2,43 +2,43 @@
.token.prolog,
.token.doctype,
.token.cdata {
color: var(--tblr-gray-300);
color: var(--tblr-gray-300);
}
.token.punctuation {
color: var(--tblr-gray-500);
color: var(--tblr-gray-500);
}
.namespace {
opacity: .7;
opacity: 0.7;
}
.token.property,
.token.tag {
color: #f92672;
color: #f92672;
/* We need to reset the 'tag' styles set by tabler */
border: 0;
display: inherit;
height: inherit;
border-radius: inherit;
padding: 0;
background: inherit;
box-shadow: inherit;
/* We need to reset the 'tag' styles set by tabler */
border: 0;
display: inherit;
height: inherit;
border-radius: inherit;
padding: 0;
background: inherit;
box-shadow: inherit;
}
.token.number {
color: #ea9999;
color: #ea9999;
}
.token.boolean {
color: #ae81ff;
color: #ae81ff;
}
.token.selector,
.token.attr-name,
.token.string {
color: #97e1a3;
color: #97e1a3;
}
.token.operator,
@@ -46,47 +46,47 @@
.token.url,
.language-css .token.string,
.style .token.string {
color: #f8f8f2;
color: #f8f8f2;
}
.token.atrule,
.token.attr-value
{
color: #e6db74;
.token.attr-value {
color: #e6db74;
}
.token.keyword{
color: #95d1ff;
.token.keyword {
color: #95d1ff;
}
.token.regex,
.token.important {
color: var(--tblr-yellow);
color: var(--tblr-yellow);
}
.token.important {
font-weight: bold;
font-weight: bold;
}
.token.entity {
cursor: help;
cursor: help;
}
.token {
transition: .3s;
transition: .3s;
}
code::selection, code ::selection {
background: var(--tblr-yellow);
color: var(--tblr-gray-900);
border-radius: .1em;
code::selection,
code ::selection {
background: var(--tblr-yellow);
color: var(--tblr-gray-900);
border-radius: .1em;
}
code .token.keyword::selection, code .token.punctuation::selection {
color: var(--tblr-gray-800);
code .token.keyword::selection,
code .token.punctuation::selection {
color: var(--tblr-gray-800);
}
pre code {
padding: 0;
}
padding: 0;
}
@@ -11,13 +11,13 @@
}
div.dropdown-menu- {
min-width: inherit !important;
min-width: inherit !important;
}
a.dropdown-item- {
min-width: inherit !important;
min-width: inherit !important;
}
.slim_item {
min-width: inherit !important;
min-width: inherit !important;
}
@@ -1,3 +1,3 @@
{
"database_url": "postgres://my_username:my_password@localhost:5432/my_geographic_database"
}
"database_url": "postgres://my_username:my_password@localhost:5432/my_geographic_database"
}
@@ -1,3 +1,3 @@
{
"content_security_policy": "script-src blob: 'self' https://cdn.jsdelivr.net;"
}
"content_security_policy": "script-src blob: 'self' https://cdn.jsdelivr.net;"
}
@@ -1,34 +1,34 @@
const cdn = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/';
const cdn = "https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/";
function on_monaco_load() {
// Create an editor div, display it after the '#code-editor' textarea, hide the textarea, and create a Monaco editor in the div with the contents of the textarea
// When the form is submitted, set the value of the textarea to the value of the Monaco editor
const textarea = document.getElementById('code-editor');
const editorDiv = document.createElement('div');
editorDiv.style.width = '100%';
editorDiv.style.height = '700px';
textarea.parentNode.insertBefore(editorDiv, textarea.nextSibling);
const monacoConfig = {
value: textarea.value,
language: 'sql',
};
// Create an editor div, display it after the '#code-editor' textarea, hide the textarea, and create a Monaco editor in the div with the contents of the textarea
// When the form is submitted, set the value of the textarea to the value of the Monaco editor
const textarea = document.getElementById("code-editor");
const editorDiv = document.createElement("div");
editorDiv.style.width = "100%";
editorDiv.style.height = "700px";
textarea.parentNode.insertBefore(editorDiv, textarea.nextSibling);
const monacoConfig = {
value: textarea.value,
language: "sql",
};
self.MonacoEnvironment = {
baseUrl: cdn + 'min/',
};
const editor = monaco.editor.create(editorDiv, monacoConfig);
textarea.style.display = 'none';
const form = textarea.form;
form.addEventListener('submit', () => {
textarea.value = editor.getValue();
});
self.MonacoEnvironment = {
baseUrl: `${cdn}min/`,
};
const editor = monaco.editor.create(editorDiv, monacoConfig);
textarea.style.display = "none";
const form = textarea.form;
form.addEventListener("submit", () => {
textarea.value = editor.getValue();
});
}
function set_require_config() {
require.config({ paths: { vs: cdn + 'min/vs' } });
require(['vs/editor/editor.main'], on_monaco_load);
require.config({ paths: { vs: `${cdn}min/vs` } });
require(["vs/editor/editor.main"], on_monaco_load);
}
const loader_script = document.createElement('script');
loader_script.src = cdn + 'min/vs/loader.js';
const loader_script = document.createElement("script");
loader_script.src = `${cdn}min/vs/loader.js`;
loader_script.onload = set_require_config;
document.head.appendChild(loader_script);
document.head.appendChild(loader_script);
@@ -1,3 +1,3 @@
{
"max_uploaded_file_size": 5000000
}
"max_uploaded_file_size": 5000000
}
@@ -1,5 +1,3 @@
{
"sqlite_extensions": [
"mod_spatialite"
]
}
"sqlite_extensions": ["mod_spatialite"]
}
@@ -1,11 +1,7 @@
{
"component": "shell",
"title": "SQLPage Questions",
"icon": "help-hexagon",
"link": "/index.sql",
"menu_item": [
"index",
"results",
"admin"
]
}
"component": "shell",
"title": "SQLPage Questions",
"icon": "help-hexagon",
"link": "/index.sql",
"menu_item": ["index", "results", "admin"]
}
+1 -1
View File
@@ -4,4 +4,4 @@
"max_uploaded_file_size": 10485760,
"compress_responses": false,
"environment": "production"
}
}
+1 -1
View File
@@ -1 +1 @@
hljs.highlightAll()
hljs.highlightAll();
@@ -3,46 +3,46 @@
.hljs-prolog,
.hljs-meta,
.hljs-cdata {
color: #a598a3;
color: #a598a3;
}
/* Punctuation */
.hljs-template-variable,
.hljs-punctuation {
color: #e9eac7;
color: #e9eac7;
}
/* Namespace */
.hljs-namespace {
opacity: .7;
opacity: 0.7;
}
/* Property and Tag */
.hljs-property {
color: #de5f8f;
color: #de5f8f;
}
/* Number */
.hljs-number {
color: #ea9999;
color: #ea9999;
}
/* Boolean */
.hljs-literal {
color: #ae81ff;
color: #ae81ff;
}
/* Selector, Attr-name, and String */
.hljs-attr {
color: #fcfce5;
color: #fcfce5;
}
.hljs-name {
color: #e4faf6;
color: #e4faf6;
}
.hljs-selector-tag,
.hljs-string {
color: #97e1a3;
color: #97e1a3;
}
/* Operator, Entity, URL, CSS String, and Style String */
@@ -51,55 +51,57 @@
.hljs-link,
.language-css .hljs-string,
.style .hljs-string {
color: #f8f8f2;
color: #f8f8f2;
}
/* At-rule and Attr-value */
.hljs-tag,
.hljs-keyword,
.hljs-attribute-value {
color: #e6db74;
color: #e6db74;
}
/* Keyword */
.hljs-template-tag,
.hljs-keyword {
color: #95d1ff;
color: #95d1ff;
}
/* Regex and Important */
.hljs-regexp,
.hljs-important {
color: var(--tblr-yellow);
color: var(--tblr-yellow);
}
/* Important */
.hljs-important {
font-weight: bold;
font-weight: bold;
}
/* Entity */
.hljs-symbol {
cursor: help;
cursor: help;
}
/* Token transition */
.hljs {
transition: .3s;
transition: .3s;
}
/* Code selection */
code::selection, code ::selection {
background: var(--tblr-yellow);
color: var(--tblr-gray-900);
border-radius: .1em;
code::selection,
code ::selection {
background: var(--tblr-yellow);
color: var(--tblr-gray-900);
border-radius: .1em;
}
code .hljs-keyword::selection, code .hljs-punctuation::selection {
color: var(--tblr-gray-800);
code .hljs-keyword::selection,
code .hljs-punctuation::selection {
color: var(--tblr-gray-800);
}
/* Pre code padding */
pre code {
padding: 0;
padding: 0;
}
+17 -16
View File
@@ -1,16 +1,17 @@
/* Ensure all unordered and ordered lists are left-aligned */
ul, ol {
text-align: left; /* Aligns bullet points to the left */
margin-left: 20px; /* Adds indentation for better readability */
padding-left: 20px; /* Adds space between bullet and text */
}
/* Optional: Style for the individual list items */
li {
margin-bottom: 10px; /* Adds space between list items */
}
/* Optional: Ensure the body text is also left-aligned */
body {
text-align: left; /* Makes sure the overall page is left-aligned */
}
/* Ensure all unordered and ordered lists are left-aligned */
ul,
ol {
text-align: left; /* Aligns bullet points to the left */
margin-left: 20px; /* Adds indentation for better readability */
padding-left: 20px; /* Adds space between bullet and text */
}
/* Optional: Style for the individual list items */
li {
margin-bottom: 10px; /* Adds space between list items */
}
/* Optional: Ensure the body text is also left-aligned */
body {
text-align: left; /* Makes sure the overall page is left-aligned */
}
@@ -1,3 +1,3 @@
{
"database_url": "sqlite://:memory:"
}
"database_url": "sqlite://:memory:"
}
@@ -1,3 +1,3 @@
{
"database_url": "sqlite://:memory:"
}
"database_url": "sqlite://:memory:"
}
File diff suppressed because it is too large Load Diff
@@ -2,25 +2,28 @@
// version of a React component written in JSX.
function MyComponent({ greeting_name }) {
const [count, setCount] = React.useState(0);
return React.createElement(
'button',
{
onClick: async () => {
const r = await fetch('/api.sql');
const { total_clicks } = await r.json();
setCount(total_clicks)
},
className: 'btn btn-primary'
},
count == 0
? `Hello, ${greeting_name}. Click me !`
: `You clicked me ${count} times!`
);
const [count, setCount] = React.useState(0);
return React.createElement(
"button",
{
type: "button",
onClick: async () => {
const r = await fetch("/api.sql");
const { total_clicks } = await r.json();
setCount(total_clicks);
},
className: "btn btn-primary",
},
count === 0
? `Hello, ${greeting_name}. Click me !`
: `You clicked me ${count} times!`,
);
}
for (const container of document.getElementsByClassName('react_component')) {
const root = ReactDOM.createRoot(container);
const props = JSON.parse(container.dataset.props);
root.render(React.createElement(window[props.react_component_name], props, null));
}
for (const container of document.getElementsByClassName("react_component")) {
const root = ReactDOM.createRoot(container);
const props = JSON.parse(container.dataset.props);
root.render(
React.createElement(window[props.react_component_name], props, null),
);
}
@@ -16,7 +16,8 @@
}
@keyframes neon-glow {
0%, 100% {
0%,
100% {
text-shadow: 0 0 10px #fa2dd1, 0 0 20px #b30890, 2px 3px 30px #ff00cc;
}
50% {
@@ -29,5 +30,5 @@
}
#funky_text * {
color: inherit;
}
color: inherit;
}
+263 -212
View File
@@ -1,231 +1,282 @@
/* !include https://cdn.jsdelivr.net/npm/apexcharts@4.0.0/dist/apexcharts.min.js */
sqlpage_chart = (() => {
function sqlpage_chart() {
for (const c of document.querySelectorAll("[data-pre-init=chart]")) {
try {
build_sqlpage_chart(c);
} catch (e) {
console.log(e);
}
}
}
function sqlpage_chart() {
for (const c of document.querySelectorAll("[data-pre-init=chart]")) {
try { build_sqlpage_chart(c) }
catch (e) { console.log(e) }
const tblrColors = Object.fromEntries(
[
"azure",
"red",
"lime",
"purple",
"yellow",
"gray-600",
"orange",
"pink",
"teal",
"indigo",
"cyan",
"green",
"blue-lt",
"yellow-lt",
"pink-lt",
"green-lt",
"orange-lt",
"blue",
"gray-500",
"gray-400",
"gray-300",
"gray-200",
"gray-100",
"gray-50",
"black",
].map((c) => [
c,
getComputedStyle(document.documentElement).getPropertyValue(
`--tblr-${c}`,
),
]),
);
/** @typedef { { [name:string]: {data:{x:number,y:number}[], name:string} } } Series */
/**
* @param {Series} series
* @returns {Series} */
function align_categories(series) {
const new_series = series.map((s) => ({ name: s.name, data: [] }));
do {
let category = null;
series.forEach((s, s_i) => {
const point = s.data[0];
let new_point = { x: category, y: 0 };
if (point) {
if (category == null) category = point.x;
if (category === point.x) {
new_point = s.data.shift();
}
}
new_series[s_i].data.push(new_point);
});
for (const s of new_series) {
s.data[s.data.length - 1].x = category;
}
} while (category != null);
for (const s of new_series) {
s.data.pop();
}
return new_series;
}
const tblrColors = Object.fromEntries(['azure', 'red', 'lime', 'purple', 'yellow', 'gray-600', 'orange', 'pink', 'teal', 'indigo', 'cyan', 'green', 'blue-lt', 'yellow-lt', 'pink-lt', 'green-lt', 'orange-lt', 'blue', 'gray-500', 'gray-400', 'gray-300', 'gray-200', 'gray-100', 'gray-50', 'black']
.map(c => [c, getComputedStyle(document.documentElement).getPropertyValue('--tblr-' + c)]));
/** @typedef { { [name:string]: {data:{x:number,y:number}[], name:string} } } Series */
/**
* @param {Series} series
* @returns {Series} */
function align_categories(series) {
const new_series = series.map(s => ({ name: s.name, data: [] }));
do {
var category = null;
series.forEach((s, s_i) => {
const point = s.data[0];
let new_point = { x: category, y: 0 };
if (point) {
if (category == null) category = point.x;
if (category === point.x) {
new_point = s.data.shift();
}
}
new_series[s_i].data.push(new_point);
})
new_series.forEach(s => s.data[s.data.length - 1].x = category);
} while (category != null);
new_series.forEach(s => s.data.pop());
return new_series;
/** @param {HTMLElement} c */
function build_sqlpage_chart(c) {
const [data_element] = c.getElementsByTagName("data");
const data = JSON.parse(data_element.textContent);
const chartContainer = c.querySelector(".chart");
chartContainer.innerHTML = "";
const is_timeseries = !!data.time;
/** @type { Series } */
const series_map = {};
for (const [name, old_x, old_y, z] of data.points) {
series_map[name] = series_map[name] || { name, data: [] };
let x = old_x;
let y = old_y;
if (is_timeseries) {
if (typeof x === "number") x = new Date(x * 1000);
else if (data.type === "rangeBar" && Array.isArray(y))
y = y.map((y) => new Date(y).getTime());
else x = new Date(x);
}
series_map[name].data.push({ x, y, z });
}
if (data.xmin == null) data.xmin = undefined;
if (data.xmax == null) data.xmax = undefined;
if (data.ymin == null) data.ymin = undefined;
if (data.ymax == null) data.ymax = undefined;
const colors = [
...data.colors.filter((c) => c).map((c) => tblrColors[c]),
...Object.values(tblrColors),
];
/** @param {HTMLElement} c */
function build_sqlpage_chart(c) {
const [data_element] = c.getElementsByTagName("data");
const data = JSON.parse(data_element.textContent);
const chartContainer = c.querySelector('.chart');
chartContainer.innerHTML = "";
const is_timeseries = !!data.time;
/** @type { Series } */
const series_map = {};
data.points.forEach(([name, x, y, z]) => {
series_map[name] = series_map[name] || { name, data: [] }
if (is_timeseries) {
if (typeof x === 'number') x = new Date(x * 1000) // databases use seconds; JS uses ms
else if (data.type === 'rangeBar' && Array.isArray(y)) y = y.map(y => new Date(y).getTime()); // timerange charts
else x = new Date(x);
let series = Object.values(series_map);
// tickamount is the number of intervals, not the number of ticks
const tickAmount =
data.xticks ||
Math.min(30, Math.max(...series.map((s) => s.data.length - 1)));
let labels;
const categories =
series.length > 0 && typeof series[0].data[0].x === "string";
if (data.type === "pie") {
labels = data.points.map(([name, x, y]) => x || name);
series = data.points.map(([name, x, y]) => y);
} else if (categories && data.type === "bar")
series = align_categories(series);
const options = {
chart: {
type: data.type || "line",
fontFamily: "inherit",
parentHeightOffset: 0,
height: chartContainer.style.height,
stacked: !!data.stacked,
toolbar: {
show: !!data.toolbar,
},
animations: {
enabled: false,
},
zoom: {
enabled: false,
},
},
theme: {
palette: "palette4",
},
dataLabels: {
enabled: !!data.labels,
dropShadow: {
enabled: true,
color: "#f6f8fb",
},
formatter:
data.type === "rangeBar"
? (_val, { seriesIndex, w }) => w.config.series[seriesIndex].name
: data.type === "pie"
? (value, { seriesIndex, w }) =>
`${w.config.labels[seriesIndex]}: ${value.toFixed()}%`
: (value) => value.toLocaleString(),
},
fill: {
type: data.type === "area" ? "gradient" : "solid",
},
stroke: {
width: data.type === "area" ? 3 : 1,
lineCap: "round",
curve: "smooth",
},
xaxis: {
tooltip: {
enabled: false,
},
min: data.xmin,
max: data.xmax,
tickAmount,
title: {
text: data.xtitle || undefined,
},
type: is_timeseries ? "datetime" : categories ? "category" : undefined,
labels: {
datetimeUTC: false,
},
},
yaxis: {
logarithmic: !!data.logarithmic,
min: data.ymin,
max: data.ymax,
stepSize: data.ystep,
tickAmount: data.yticks,
title: {
text: data.ytitle || undefined,
},
},
zaxis: {
title: {
text: data.ztitle || undefined,
},
},
markers: {
size: data.marker || 0,
strokeWidth: 0,
hover: {
sizeOffset: 5,
},
},
tooltip: {
fillSeriesColor: false,
custom:
data.type === "bubble" || data.type === "scatter"
? bubbleTooltip
: undefined,
y: {
formatter: (value) => {
if (is_timeseries && data.type === "rangeBar") {
const d = new Date(value);
if (d.getHours() === 0 && d.getMinutes() === 0)
return d.toLocaleDateString();
return d.toLocaleString();
}
series_map[name].data.push({ x, y, z });
})
if (data.xmin == null) data.xmin = undefined;
if (data.xmax == null) data.xmax = undefined;
if (data.ymin == null) data.ymin = undefined;
if (data.ymax == null) data.ymax = undefined;
const str_val = value.toLocaleString();
if (str_val.length > 10 && Number.isNaN(value))
return value.toFixed(2);
return str_val;
},
},
},
plotOptions: {
bar: {
horizontal: !!data.horizontal || data.type === "rangeBar",
borderRadius: 5,
},
bubble: { minBubbleRadius: 5 },
},
colors,
series,
};
if (labels) options.labels = labels;
const chart = new ApexCharts(chartContainer, options);
chart.render();
if (window.charts) window.charts.push(chart);
else window.charts = [chart];
c.removeAttribute("data-pre-init");
}
const colors = [
...data.colors.filter(c => c).map(c => tblrColors[c]),
...Object.values(tblrColors)
];
function bubbleTooltip({ series, seriesIndex, dataPointIndex, w }) {
const { name, data } = w.config.series[seriesIndex];
const point = data[dataPointIndex];
let series = Object.values(series_map);
const tooltip = document.createElement("div");
tooltip.className = "apexcharts-tooltip-text";
tooltip.style.fontFamily = "inherit";
// tickamount is the number of intervals, not the number of ticks
const tickAmount = data.xticks ||
Math.min(30, Math.max(...series.map(s => s.data.length - 1)));
const seriesName = document.createElement("div");
seriesName.className = "apexcharts-tooltip-y-group";
seriesName.style.fontWeight = "bold";
seriesName.innerText = name;
tooltip.appendChild(seriesName);
let labels;
const categories = series.length > 0 && typeof series[0].data[0].x === "string";
if (data.type === "pie") {
labels = data.points.map(([name, x, y]) => x || name);
series = data.points.map(([name, x, y]) => y);
} else if (categories && data.type === 'bar') series = align_categories(series);
const options = {
chart: {
type: data.type || 'line',
fontFamily: 'inherit',
parentHeightOffset: 0,
height: chartContainer.style.height,
stacked: !!data.stacked,
toolbar: {
show: !!data.toolbar,
},
animations: {
enabled: false
},
zoom: {
enabled: false
}
},
theme: {
palette: 'palette4',
},
dataLabels: {
enabled: !!data.labels,
dropShadow: {
enabled: true,
color: '#f6f8fb',
},
formatter: (data.type === 'rangeBar') ? (_val, { seriesIndex, w }) => w.config.series[seriesIndex].name :
(data.type === 'pie') ? (value, { seriesIndex, w }) => `${w.config.labels[seriesIndex]}: ${value.toFixed()}%`
: value => value.toLocaleString(),
},
fill: {
type: data.type === 'area' ? 'gradient' : 'solid',
},
stroke: {
width: data.type === 'area' ? 3 : 1,
lineCap: "round",
curve: "smooth",
},
xaxis: {
tooltip: {
enabled: false
},
min: data.xmin,
max: data.xmax,
tickAmount,
title: {
text: data.xtitle || undefined,
},
type: is_timeseries ? 'datetime' : categories ? 'category' : undefined,
labels: {
datetimeUTC: false,
}
},
yaxis: {
logarithmic: !!data.logarithmic,
min: data.ymin,
max: data.ymax,
stepSize: data.ystep,
tickAmount: data.yticks,
title: {
text: data.ytitle || undefined,
}
},
zaxis: {
title: {
text: data.ztitle || undefined,
}
},
markers: {
size: data.marker || 0,
strokeWidth: 0,
hover: {
sizeOffset: 5,
}
},
tooltip: {
fillSeriesColor: false,
custom: (data.type === 'bubble' || data.type === 'scatter') ? bubbleTooltip : undefined,
y: {
formatter: (value) => {
if (is_timeseries && data.type === 'rangeBar') {
const d = new Date(value);
if (d.getHours() === 0 && d.getMinutes() === 0) return d.toLocaleDateString();
return d.toLocaleString();
}
const str_val = value.toLocaleString();
if (str_val.length > 10 && value != value | 0) return value.toFixed(2);
return str_val;
}
}
},
plotOptions: {
bar: {
horizontal: !!data.horizontal || data.type === 'rangeBar',
borderRadius: 5,
},
bubble: { minBubbleRadius: 5, },
},
colors,
series,
};
if (labels) options.labels = labels;
const chart = new ApexCharts(chartContainer, options);
chart.render();
if (window.charts) window.charts.push(chart);
else window.charts = [chart];
c.removeAttribute("data-pre-init");
for (const axis of ["x", "y", "z"]) {
const value = point[axis];
if (value == null) continue;
const axisValue = document.createElement("div");
axisValue.className = "apexcharts-tooltip-y-group";
let axis_conf = w.config[`${axis}axis`];
if (axis_conf.length) axis_conf = axis_conf[0];
const title = axis_conf.title.text || axis;
const labelSpan = document.createElement("span");
labelSpan.className = "apexcharts-tooltip-text-y-label";
labelSpan.innerText = `${title}: `;
axisValue.appendChild(labelSpan);
const valueSpan = document.createElement("span");
valueSpan.className = "apexcharts-tooltip-text-y-value";
valueSpan.innerText = value;
axisValue.appendChild(valueSpan);
tooltip.appendChild(axisValue);
}
return tooltip.outerHTML;
}
function bubbleTooltip({ series, seriesIndex, dataPointIndex, w }) {
const { name, data } = w.config.series[seriesIndex];
const point = data[dataPointIndex];
const tooltip = document.createElement('div');
tooltip.className = 'apexcharts-tooltip-text';
tooltip.style.fontFamily = 'inherit';
const seriesName = document.createElement('div');
seriesName.className = 'apexcharts-tooltip-y-group';
seriesName.style.fontWeight = 'bold';
seriesName.innerText = name;
tooltip.appendChild(seriesName);
for (const axis of ['x', 'y', 'z']) {
const value = point[axis];
if (value == null) continue;
const axisValue = document.createElement('div');
axisValue.className = 'apexcharts-tooltip-y-group';
let axis_conf = w.config[axis + 'axis'];
if (axis_conf.length) axis_conf = axis_conf[0];
const title = axis_conf.title.text || axis;
const labelSpan = document.createElement('span');
labelSpan.className = 'apexcharts-tooltip-text-y-label';
labelSpan.innerText = title + ': ';
axisValue.appendChild(labelSpan);
const valueSpan = document.createElement('span');
valueSpan.className = 'apexcharts-tooltip-text-y-value';
valueSpan.innerText = value;
axisValue.appendChild(valueSpan);
tooltip.appendChild(axisValue);
}
return tooltip.outerHTML;
}
return sqlpage_chart;
return sqlpage_chart;
})();
add_init_fn(sqlpage_chart);
add_init_fn(sqlpage_chart);
+3 -2
View File
@@ -58,7 +58,8 @@ code {
font-size: 100%;
}
.apexcharts-text, .apexcharts-datalabel {
.apexcharts-text,
.apexcharts-datalabel {
fill: var(--tblr-body-color) !important;
font-weight: var(--tblr-body-font-weight);
}
}
+84 -43
View File
@@ -5,8 +5,8 @@ function sqlpage_card() {
for (const c of document.querySelectorAll("[data-pre-init=card]")) {
const source = c.dataset.embed;
fetch(c.dataset.embed)
.then(res => res.text())
.then(html => {
.then((res) => res.text())
.then((html) => {
const body = c.querySelector(".card-content");
body.innerHTML = html;
c.removeAttribute("data-pre-init");
@@ -15,10 +15,10 @@ function sqlpage_card() {
spinner.parentNode.removeChild(spinner);
}
const fragLoadedEvt = new CustomEvent("fragment-loaded", {
bubbles: true
bubbles: true,
});
c.dispatchEvent(fragLoadedEvt);
})
});
}
}
@@ -28,19 +28,25 @@ function table_search_sort(el) {
const search_input = el.querySelector("input.search");
const sort_buttons = [...el.querySelectorAll("button.sort[data-sort]")];
const item_parent = el.querySelector("tbody");
const items = [...item_parent.querySelectorAll("tr")].map(el => ({
const items = [...item_parent.querySelectorAll("tr")].map((el) => ({
el,
sort_keys: sort_buttons.map(b => {
const sort_key = el.getElementsByClassName(b.dataset.sort)[0]?.textContent;
sort_keys: sort_buttons.map((b) => {
const sort_key = el.getElementsByClassName(b.dataset.sort)[0]
?.textContent;
return { num: Number.parseFloat(sort_key), str: sort_key };
})
}),
}));
function onSearch() {
const lower_search = search_input.value.toLowerCase().split(/\s+/).filter(s => s);
items.forEach(item => {
const show = lower_search.every(s => item.el.textContent.toLowerCase().includes(s));
const lower_search = search_input.value
.toLowerCase()
.split(/\s+/)
.filter((s) => s);
for (const item of items) {
const show = lower_search.every((s) =>
item.el.textContent.toLowerCase().includes(s),
);
item.el.style.display = show ? "" : "none";
});
}
}
if (search_input) {
search_input.addEventListener("input", onSearch);
@@ -49,15 +55,22 @@ function table_search_sort(el) {
sort_buttons.forEach((button, button_index) => {
button.addEventListener("click", function sort_items() {
const sort_desc = button.classList.contains("asc");
sort_buttons.forEach(b => b.classList.remove("asc", "desc"));
for (const b of sort_buttons) {
b.classList.remove("asc", "desc");
}
button.classList.add(sort_desc ? "desc" : "asc");
const multiplier = sort_desc ? -1 : 1;
items.sort((a, b) => {
const a_key = a.sort_keys[button_index];
const b_key = b.sort_keys[button_index];
return multiplier * ((isNaN(a_key.num) || isNaN(b_key.num)) ? a_key.str.localeCompare(b_key.str) : a_key.num - b_key.num);
return (
multiplier *
(Number.isNaN(a_key.num) || Number.isNaN(b_key.num)
? a_key.str.localeCompare(b_key.str)
: a_key.num - b_key.num)
);
});
item_parent.append(...items.map(item => item.el));
item_parent.append(...items.map((item) => item.el));
});
});
}
@@ -79,13 +92,17 @@ function sqlpage_map() {
// Add the leaflet js and css to the page
const leaflet_css = document.createElement("link");
leaflet_css.rel = "stylesheet";
leaflet_css.href = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css";
leaflet_css.integrity = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
leaflet_css.href =
"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css";
leaflet_css.integrity =
"sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
leaflet_css.crossOrigin = "anonymous";
document.head.appendChild(leaflet_css);
const leaflet_js = document.createElement("script");
leaflet_js.src = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js";
leaflet_js.integrity = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
leaflet_js.src =
"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js";
leaflet_js.integrity =
"sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
leaflet_js.crossOrigin = "anonymous";
leaflet_js.nonce = nonce;
leaflet_js.onload = onLeafletLoad;
@@ -96,7 +113,7 @@ function sqlpage_map() {
onLeafletLoad();
}
function parseCoords(coords) {
return coords && coords.split(",").map(c => Number.parseFloat(c));
return coords?.split(",").map((c) => Number.parseFloat(c));
}
function onLeafletLoad() {
is_leaflet_loaded = true;
@@ -108,16 +125,19 @@ function sqlpage_map() {
const map = L.map(m, { attributionControl: !!attribution });
const zoom = m.dataset.zoom;
const center = parseCoords(m.dataset.center);
if (tile_source) L.tileLayer(tile_source, { attribution, maxZoom }).addTo(map);
if (tile_source)
L.tileLayer(tile_source, { attribution, maxZoom }).addTo(map);
map._sqlpage_markers = [];
for (const marker_elem of m.getElementsByClassName("marker")) {
setTimeout(addMarker, 0, marker_elem, map);
}
setTimeout(() => {
if (center == null && map._sqlpage_markers.length) {
map.fitBounds(map._sqlpage_markers.map(m =>
m.getLatLng ? m.getLatLng() : m.getBounds()
));
map.fitBounds(
map._sqlpage_markers.map((m) =>
m.getLatLng ? m.getLatLng() : m.getBounds(),
),
);
if (zoom != null) map.setZoom(+zoom);
} else map.setView(center, +zoom);
}, 100);
@@ -132,22 +152,28 @@ function sqlpage_map() {
color: marker_elem.dataset.color,
title: marker_elem.getElementsByTagName("h3")[0].textContent.trim(),
};
const marker =
dataset.coords ? createMarker(marker_elem, options)
: createGeoJSONMarker(marker_elem, options);
const marker = dataset.coords
? createMarker(marker_elem, options)
: createGeoJSONMarker(marker_elem, options);
marker.addTo(map);
map._sqlpage_markers.push(marker);
if (options.title) marker.bindPopup(marker_elem);
else if (marker_elem.dataset.link) marker.on('click', () => window.location = marker_elem.dataset.link);
else if (marker_elem.dataset.link) {
marker.on("click", () => {
window.location.href = marker_elem.dataset.link;
});
}
}
function createMarker(marker_elem, options) {
const coords = parseCoords(marker_elem.dataset.coords);
const icon_obj = marker_elem.getElementsByClassName("mapicon")[0];
if (icon_obj) {
const size = 1.5 * +(options.size || icon_obj.firstChild?.getAttribute('width') || 24);
const size =
1.5 *
+(options.size || icon_obj.firstChild?.getAttribute("width") || 24);
options.icon = L.divIcon({
html: icon_obj,
className: `border-0 bg-${options.color || 'primary'} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`,
className: `border-0 bg-${options.color || "primary"} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
});
@@ -164,7 +190,7 @@ function sqlpage_map() {
return { ...options, ...properties };
}
function pointToLayer(feature, latlng) {
marker_elem.dataset.coords = latlng.lat + "," + latlng.lng;
marker_elem.dataset.coords = `${latlng.lat},${latlng.lng}`;
return createMarker(marker_elem, { ...options, ...feature.properties });
}
return L.geoJSON(geojson, { style, pointToLayer });
@@ -172,7 +198,9 @@ function sqlpage_map() {
}
function sqlpage_form() {
const file_inputs = document.querySelectorAll("input[type=file][data-max-size]");
const file_inputs = document.querySelectorAll(
"input[type=file][data-max-size]",
);
for (const input of file_inputs) {
const max_size = +input.dataset.maxSize;
input.addEventListener("change", function () {
@@ -181,7 +209,9 @@ function sqlpage_form() {
for (const { size } of this.files) {
if (size > max_size) {
input.classList.add("is-invalid");
return input.setCustomValidity(`File size must be less than ${max_size / 1000} kB.`);
return input.setCustomValidity(
`File size must be less than ${max_size / 1000} kB.`,
);
}
}
});
@@ -189,12 +219,16 @@ function sqlpage_form() {
}
function get_tabler_color(name) {
return getComputedStyle(document.documentElement).getPropertyValue('--tblr-' + name);
return getComputedStyle(document.documentElement).getPropertyValue(
`--tblr-${name}`,
);
}
function load_scripts() {
const addjs = document.querySelectorAll("[data-sqlpage-js]");
const existing_scripts = new Set([...document.querySelectorAll("script")].map(s => s.src));
const existing_scripts = new Set(
[...document.querySelectorAll("script")].map((s) => s.src),
);
for (const el of addjs) {
const js = new URL(el.dataset.sqlpageJs, window.location.href).href;
if (existing_scripts.has(js)) continue;
@@ -206,12 +240,11 @@ function load_scripts() {
}
function add_init_fn(f) {
document.addEventListener('DOMContentLoaded', f);
document.addEventListener('fragment-loaded', f);
document.addEventListener("DOMContentLoaded", f);
document.addEventListener("fragment-loaded", f);
if (document.readyState !== "loading") setTimeout(f, 0);
}
add_init_fn(sqlpage_table);
add_init_fn(sqlpage_map);
add_init_fn(sqlpage_card);
@@ -221,11 +254,19 @@ add_init_fn(load_scripts);
function init_bootstrap_components(event) {
if (window.bootstrap) {
const fragment = event.target;
fragment.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el));
fragment.querySelectorAll('[data-bs-toggle="popover"]').forEach(el => new bootstrap.Popover(el));
fragment.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(el => new bootstrap.Dropdown(el));
fragment.querySelectorAll('[data-bs-ride="carousel"]').forEach(el => new bootstrap.Carousel(el));
for (const el of fragment.querySelectorAll('[data-bs-toggle="tooltip"]')) {
new bootstrap.Tooltip(el);
}
for (const el of fragment.querySelectorAll('[data-bs-toggle="popover"]')) {
new bootstrap.Popover(el);
}
for (const el of fragment.querySelectorAll('[data-bs-toggle="dropdown"]')) {
new bootstrap.Dropdown(el);
}
for (const el of fragment.querySelectorAll('[data-bs-ride="carousel"]')) {
new bootstrap.Carousel(el);
}
}
}
document.addEventListener('fragment-loaded', init_bootstrap_components);
document.addEventListener("fragment-loaded", init_bootstrap_components);
+4 -2
View File
@@ -1,7 +1,9 @@
/* !include https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.popular.min.js */
function sqlpage_select_dropdown() {
for (const s of document.querySelectorAll("[data-pre-init=select-dropdown]")) {
for (const s of document.querySelectorAll(
"[data-pre-init=select-dropdown]",
)) {
// See: https://github.com/orchidjs/tom-select/issues/716
// By default, TomSelect will not retain the focus if s is already focused
// This is a workaround to fix that
@@ -15,4 +17,4 @@ function sqlpage_select_dropdown() {
}
}
add_init_fn(sqlpage_select_dropdown);
add_init_fn(sqlpage_select_dropdown);
+77 -72
View File
@@ -1,134 +1,139 @@
import { test, expect } from '@playwright/test';
import { expect, test } from "@playwright/test";
const BASE = 'http://localhost:8080/';
const BASE = "http://localhost:8080/";
test('Open documentation', async ({ page }) => {
test("Open documentation", async ({ page }) => {
await page.goto(BASE);
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/SQLPage.*/);
// open the submenu
await page.getByText('Documentation', { exact: true }).first().click();
await page.getByText('All Components').click();
const components = ['form', 'map', 'chart', 'button'];
await page.getByText("Documentation", { exact: true }).first().click();
await page.getByText("All Components").click();
const components = ["form", "map", "chart", "button"];
for (const component of components) {
await expect(page.getByRole('link', { name: component }).first()).toBeVisible();
await expect(
page.getByRole("link", { name: component }).first(),
).toBeVisible();
}
});
test('chart', async ({ page }) => {
await page.goto(BASE + '/documentation.sql?component=chart#component');
await expect(page.getByText('Loading...')).not.toBeVisible();
await expect(page.locator('.apexcharts-canvas').first()).toBeVisible();
test("chart", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=chart#component`);
await expect(page.getByText("Loading...")).not.toBeVisible();
await expect(page.locator(".apexcharts-canvas").first()).toBeVisible();
});
test('map', async ({ page }) => {
await page.goto(BASE + '/documentation.sql?component=map#component');
await expect(page.getByText('Loading...')).not.toBeVisible();
await expect(page.locator('.leaflet-marker-icon').first()).toBeVisible();
test("map", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=map#component`);
await expect(page.getByText("Loading...")).not.toBeVisible();
await expect(page.locator(".leaflet-marker-icon").first()).toBeVisible();
});
test('form example', async ({ page }) => {
await page.goto(BASE + '/examples/multistep-form');
test("form example", async ({ page }) => {
await page.goto(`${BASE}/examples/multistep-form`);
// Single selection matching the value or label
await page.getByLabel('From').selectOption('Paris');
await page.getByText('Next').click();
await page.getByLabel(/\bTo\b/).selectOption('Mexico');
await page.getByText('Next').click();
await page.getByLabel('Number of Adults').fill('1');
await page.getByText('Next').click();
await page.getByLabel('Passenger 1 (adult)').fill('John Doe');
await page.getByText('Book the flight').click();
await expect(page.getByText('John Doe').first()).toBeVisible();
await page.getByLabel("From").selectOption("Paris");
await page.getByText("Next").click();
await page.getByLabel(/\bTo\b/).selectOption("Mexico");
await page.getByText("Next").click();
await page.getByLabel("Number of Adults").fill("1");
await page.getByText("Next").click();
await page.getByLabel("Passenger 1 (adult)").fill("John Doe");
await page.getByText("Book the flight").click();
await expect(page.getByText("John Doe").first()).toBeVisible();
});
test('File upload', async ({ page }) => {
test("File upload", async ({ page }) => {
await page.goto(BASE);
await page.getByRole('button', { name: 'Examples', exact: true }).click();
await page.getByText('File uploads').click();
await page.getByRole("button", { name: "Examples", exact: true }).click();
await page.getByText("File uploads").click();
const my_svg = '<svg><text y="20">Hello World</text></svg>';
// @ts-ignore
const buffer = Buffer.from(my_svg);
await page.getByLabel('Picture').setInputFiles({
name: 'small.svg',
mimeType: 'image/svg+xml',
await page.getByLabel("Picture").setInputFiles({
name: "small.svg",
mimeType: "image/svg+xml",
buffer,
});
await page.getByRole('button', { name: 'Upload picture' }).click();
await expect(page.locator('img[src^=data]').first().getAttribute('src')).resolves.toBe('data:image/svg+xml;base64,' + buffer.toString('base64'));
await page.getByRole("button", { name: "Upload picture" }).click();
await expect(
page.locator("img[src^=data]").first().getAttribute("src"),
).resolves.toBe(`data:image/svg+xml;base64,${buffer.toString("base64")}`);
});
test('Authentication example', async ({ page }) => {
await page.goto(BASE + '/examples/authentication/login.sql');
await expect(page.locator('h1', { hasText: 'Authentication' })).toBeVisible();
test("Authentication example", async ({ page }) => {
await page.goto(`${BASE}/examples/authentication/login.sql`);
await expect(page.locator("h1", { hasText: "Authentication" })).toBeVisible();
const usernameInput = page.getByLabel('Username');
const passwordInput = page.getByLabel('Password');
const loginButton = page.getByRole('button', { name: 'Log in' });
const usernameInput = page.getByLabel("Username");
const passwordInput = page.getByLabel("Password");
const loginButton = page.getByRole("button", { name: "Log in" });
await expect(usernameInput).toBeVisible();
await expect(passwordInput).toBeVisible();
await expect(loginButton).toBeVisible();
await usernameInput.fill('admin');
await passwordInput.fill('admin');
await usernameInput.fill("admin");
await passwordInput.fill("admin");
await loginButton.click();
await expect(page.getByText('You are logged in as admin')).toBeVisible();
await expect(page.getByText("You are logged in as admin")).toBeVisible();
});
test('table filtering', async ({ page }) => {
await page.goto(BASE + '/documentation.sql?component=table');
const tableSection = page.locator('.table-responsive', {
has: page.getByRole('cell', { name: 'Chart' })
test("table filtering", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=table`);
const tableSection = page.locator(".table-responsive", {
has: page.getByRole("cell", { name: "Chart" }),
});
const searchInput = tableSection.getByPlaceholder('Search…');
await searchInput.fill('chart');
await expect(tableSection.getByRole('cell', { name: 'Chart' })).toBeVisible();
await expect(tableSection.getByRole('cell', { name: 'Table' })).not.toBeVisible();
const searchInput = tableSection.getByPlaceholder("Search…");
await searchInput.fill("chart");
await expect(tableSection.getByRole("cell", { name: "Chart" })).toBeVisible();
await expect(
tableSection.getByRole("cell", { name: "Table" }),
).not.toBeVisible();
});
test('table sorting', async ({ page }) => {
await page.goto(BASE + '/documentation.sql?component=table');
const tableSection = page.locator('.table-responsive', {
has: page.getByRole('cell', { name: '31456' })
test("table sorting", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=table`);
const tableSection = page.locator(".table-responsive", {
has: page.getByRole("cell", { name: "31456" }),
});
// Test numeric sorting on id column
await tableSection.getByRole('button', { name: 'id' }).click();
let ids = await tableSection.locator('td.id').allInnerTexts();
let numericIds = ids.map(id => Number.parseInt(id));
await tableSection.getByRole("button", { name: "id" }).click();
let ids = await tableSection.locator("td.id").allInnerTexts();
let numericIds = ids.map((id) => Number.parseInt(id));
const sortedIds = [...numericIds].sort((a, b) => a - b);
expect(numericIds).toEqual(sortedIds);
// Test reverse sorting
await tableSection.getByRole('button', { name: 'id' }).click();
ids = await tableSection.locator('td.id').allInnerTexts();
numericIds = ids.map(id => Number.parseInt(id));
await tableSection.getByRole("button", { name: "id" }).click();
ids = await tableSection.locator("td.id").allInnerTexts();
numericIds = ids.map((id) => Number.parseInt(id));
const reverseSortedIds = [...numericIds].sort((a, b) => b - a);
expect(numericIds).toEqual(reverseSortedIds);
// Test amount in stock column sorting
await tableSection.getByRole('button', { name: 'Amount in stock' }).click();
const amounts = await tableSection.locator('td.Amount').allInnerTexts();
const numericAmounts = amounts.map(amount => Number.parseInt(amount));
await tableSection.getByRole("button", { name: "Amount in stock" }).click();
const amounts = await tableSection.locator("td.Amount").allInnerTexts();
const numericAmounts = amounts.map((amount) => Number.parseInt(amount));
const sortedAmounts = [...numericAmounts].sort((a, b) => a - b);
expect(numericAmounts).toEqual(sortedAmounts);
});
test('no console errors on table page', async ({ page }) => {
test("no console errors on table page", async ({ page }) => {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') {
page.on("console", (msg) => {
if (msg.type() === "error") {
errors.push(msg.text());
}
});
await page.goto(BASE + '/documentation.sql?component=table');
await page.waitForLoadState('networkidle');
await page.goto(`${BASE}/documentation.sql?component=table`);
await page.waitForLoadState("networkidle");
expect(errors).toHaveLength(0);
});
});
+6 -6
View File
@@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test';
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
@@ -11,7 +11,7 @@ import { defineConfig, devices } from '@playwright/test';
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './.',
testDir: "./.",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -21,21 +21,21 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {