@allurereport/web-commons 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/attachments.d.ts +27 -14
  2. package/dist/attachments.js +20 -45
  3. package/dist/data.d.ts +5 -1
  4. package/dist/data.js +8 -2
  5. package/dist/filters/builders.d.ts +4 -0
  6. package/dist/filters/builders.js +160 -0
  7. package/dist/filters/index.d.ts +3 -0
  8. package/dist/filters/index.js +2 -0
  9. package/dist/filters/model.d.ts +39 -0
  10. package/dist/filters/model.js +1 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.js +7 -0
  13. package/dist/stores/loadableStore/constants.d.ts +1 -0
  14. package/dist/stores/loadableStore/constants.js +1 -0
  15. package/dist/stores/loadableStore/index.d.ts +3 -0
  16. package/dist/stores/loadableStore/index.js +2 -0
  17. package/dist/stores/loadableStore/store.d.ts +13 -0
  18. package/dist/stores/loadableStore/store.js +47 -0
  19. package/dist/stores/loadableStore/types.d.ts +7 -0
  20. package/dist/stores/loadableStore/types.js +1 -0
  21. package/dist/stores/loadableStore/utils.d.ts +2 -0
  22. package/dist/stores/loadableStore/utils.js +7 -0
  23. package/dist/stores/persister/index.d.ts +12 -0
  24. package/dist/stores/persister/index.js +36 -0
  25. package/dist/stores/router/index.d.ts +18 -0
  26. package/dist/stores/router/index.js +95 -0
  27. package/dist/stores/theme/actions.d.ts +3 -0
  28. package/dist/stores/theme/actions.js +30 -0
  29. package/dist/stores/theme/constants.d.ts +6 -0
  30. package/dist/stores/theme/constants.js +5 -0
  31. package/dist/stores/theme/index.d.ts +2 -0
  32. package/dist/stores/theme/index.js +2 -0
  33. package/dist/stores/theme/store.d.ts +8 -0
  34. package/dist/stores/theme/store.js +41 -0
  35. package/dist/stores/theme/types.d.ts +2 -0
  36. package/dist/stores/theme/types.js +1 -0
  37. package/dist/stores/theme/utils.d.ts +4 -0
  38. package/dist/stores/theme/utils.js +23 -0
  39. package/dist/stores/url/helpers.d.ts +12 -0
  40. package/dist/stores/url/helpers.js +74 -0
  41. package/dist/stores/url/index.d.ts +2 -0
  42. package/dist/stores/url/index.js +2 -0
  43. package/dist/stores/url/store.d.ts +19 -0
  44. package/dist/stores/url/store.js +45 -0
  45. package/dist/utils.d.ts +2 -0
  46. package/dist/utils.js +9 -0
  47. package/package.json +10 -5
@@ -1,21 +1,34 @@
1
- export interface Attachments {
1
+ type AttachmentImageData = {
2
+ img: string;
3
+ id?: string;
4
+ };
5
+ type AttachmentTextData = {
6
+ text: string;
7
+ };
8
+ type AttachmentVideoData = {
9
+ src: string;
10
+ id?: string;
11
+ contentType?: string;
12
+ };
13
+ type AttachmentImageDiffData = {
14
+ diff: {
15
+ actual?: string;
16
+ expected?: string;
17
+ diff?: string;
18
+ };
19
+ };
20
+ export type AttachmentData = AttachmentImageData | AttachmentTextData | AttachmentVideoData | AttachmentImageDiffData;
21
+ type AttachmentPayload = {
2
22
  id?: string;
3
23
  ext?: string;
4
24
  contentType?: string;
5
- text?: string;
6
- src?: string;
7
- img?: string;
8
- }
9
- export declare const fetchFromUrl: ({ id, ext, contentType }: Attachments) => Promise<Response>;
10
- export declare const fetchAttachment: (id: string, ext: string, contentType?: string) => Promise<Attachments | null>;
25
+ };
26
+ export declare const fetchFromUrl: ({ id, ext, contentType }: AttachmentPayload) => Promise<Response>;
27
+ export declare const fetchAttachment: (id: string, ext: string, contentType?: string) => Promise<AttachmentData | null>;
11
28
  export declare const blobAttachment: (id: string, ext: string, contentType: string) => Promise<Blob>;
12
29
  export declare const downloadAttachment: (id: string, ext: string, contentType: string) => Promise<void>;
13
30
  export declare const openAttachmentInNewTab: (id: string, ext: string, contentType: string) => Promise<void>;
14
- export declare const attachmentType: (type?: string) => {
15
- type: string;
16
- icon: string;
17
- } | {
18
- type: null;
19
- icon: string;
20
- };
31
+ export type AttachmentType = "css" | "json" | "image" | "svg" | "code" | "text" | "html" | "table" | "video" | "uri" | "archive" | "image-diff";
32
+ export declare const attachmentType: (type?: string) => AttachmentType | null;
21
33
  export declare const restrictedContentTypes: string[];
34
+ export {};
@@ -9,7 +9,10 @@ export const fetchAttachment = async (id, ext, contentType) => {
9
9
  }
10
10
  const response = await fetchFromUrl({ id, ext, contentType });
11
11
  const fileType = attachmentType(contentType);
12
- switch (fileType.type) {
12
+ if (!response.ok) {
13
+ throw new Error("Failed to fetch");
14
+ }
15
+ switch (fileType) {
13
16
  case "svg":
14
17
  case "image": {
15
18
  const blob = await response.blob();
@@ -29,6 +32,10 @@ export const fetchAttachment = async (id, ext, contentType) => {
29
32
  const src = URL.createObjectURL(blob);
30
33
  return { src, id, contentType };
31
34
  }
35
+ case "image-diff": {
36
+ const json = await response.json();
37
+ return { diff: json };
38
+ }
32
39
  default:
33
40
  return null;
34
41
  }
@@ -67,10 +74,7 @@ export const attachmentType = (type) => {
67
74
  case "image/jpg":
68
75
  case "image/png":
69
76
  case "image/*":
70
- return {
71
- type: "image",
72
- icon: "file",
73
- };
77
+ return "image";
74
78
  case "text/xml":
75
79
  case "text/json":
76
80
  case "text/yaml":
@@ -100,63 +104,34 @@ export const attachmentType = (type) => {
100
104
  case "application/x-yaml":
101
105
  case "application/xml":
102
106
  case "application/json":
103
- return {
104
- type: "code",
105
- icon: "file",
106
- };
107
+ return "code";
107
108
  case "text/plain":
108
109
  case "text/markdown":
109
110
  case "text/*":
110
- return {
111
- type: "text",
112
- icon: "txt",
113
- };
111
+ return "text";
114
112
  case "text/html":
115
- return {
116
- type: "html",
117
- icon: "file",
118
- };
113
+ return "html";
119
114
  case "text/csv":
120
- return {
121
- type: "table",
122
- icon: "csv",
123
- };
124
115
  case "text/tab-separated-values":
125
- return {
126
- type: "table",
127
- icon: "table",
128
- };
116
+ return "table";
129
117
  case "image/svg+xml":
130
- return {
131
- type: "svg",
132
- icon: "file",
133
- };
118
+ return "svg";
134
119
  case "video/mp4":
135
120
  case "video/ogg":
136
121
  case "video/webm":
137
- return {
138
- type: "video",
139
- icon: "file",
140
- };
122
+ return "video";
141
123
  case "text/uri-list":
142
- return {
143
- type: "uri",
144
- icon: "list",
145
- };
124
+ return "uri";
146
125
  case "application/x-tar":
147
126
  case "application/x-gtar":
148
127
  case "application/x-bzip2":
149
128
  case "application/gzip":
150
129
  case "application/zip":
151
- return {
152
- type: "archive",
153
- icon: "file",
154
- };
130
+ return "archive";
131
+ case "application/vnd.allure.image.diff":
132
+ return "image-diff";
155
133
  default:
156
- return {
157
- type: null,
158
- icon: "file",
159
- };
134
+ return null;
160
135
  }
161
136
  };
162
137
  export const restrictedContentTypes = ["application/gzip"];
package/dist/data.d.ts CHANGED
@@ -4,8 +4,12 @@ export declare const loadReportData: (name: string) => Promise<string>;
4
4
  export declare const reportDataUrl: (path: string, contentType?: string, params?: {
5
5
  bustCache: boolean;
6
6
  }) => Promise<string>;
7
+ export declare class ReportFetchError extends Error {
8
+ readonly response: Response;
9
+ constructor(message: string, response: Response);
10
+ }
7
11
  export declare const fetchReportJsonData: <T>(path: string, params?: {
8
12
  bustCache: boolean;
9
13
  }) => Promise<T>;
10
14
  export declare const fetchReportAttachment: (path: string, contentType?: string) => Promise<Response>;
11
- export declare const getReportOptions: <T>() => T;
15
+ export declare const getReportOptions: <T>() => Readonly<T>;
package/dist/data.js CHANGED
@@ -28,7 +28,7 @@ export const reportDataUrl = async (path, contentType = "application/octet-strea
28
28
  const baseEl = globalThis.document.head.querySelector("base")?.href ?? "https://localhost";
29
29
  const url = new URL(path, baseEl);
30
30
  const liveReloadHash = globalThis.localStorage.getItem(ALLURE_LIVE_RELOAD_HASH_STORAGE_KEY);
31
- const cacheKey = globalThis.allureReportOptions?.cacheKey;
31
+ const cacheKey = getReportOptions()?.cacheKey;
32
32
  if (liveReloadHash) {
33
33
  url.searchParams.set("live_reload_hash", liveReloadHash);
34
34
  }
@@ -37,11 +37,17 @@ export const reportDataUrl = async (path, contentType = "application/octet-strea
37
37
  }
38
38
  return url.toString();
39
39
  };
40
+ export class ReportFetchError extends Error {
41
+ constructor(message, response) {
42
+ super(message);
43
+ this.response = response;
44
+ }
45
+ }
40
46
  export const fetchReportJsonData = async (path, params) => {
41
47
  const url = await reportDataUrl(path, undefined, params);
42
48
  const res = await globalThis.fetch(url);
43
49
  if (!res.ok) {
44
- throw new Error(`Failed to fetch ${url}, response status: ${res.status}`);
50
+ throw new ReportFetchError(`Failed to fetch ${url}, response status: ${res.status}`, res);
45
51
  }
46
52
  const data = res.json();
47
53
  return data;
@@ -0,0 +1,4 @@
1
+ import type { AqlExpression } from "@allurereport/aql";
2
+ import { type Filter } from "./model.js";
3
+ export declare const buildFieldFilters: <T extends string = string>(filters: Filter<T>[]) => AqlExpression;
4
+ export declare const buildFilterPredicate: <Keys extends string = string>(filters: Filter<Keys>[]) => (item: Record<Keys, any>) => boolean;
@@ -0,0 +1,160 @@
1
+ import { aqlArrayConditionExpression, aqlBinaryExpression, aqlConditionExpression, aqlParenExpression, createAqlPredicate, } from "@allurereport/aql";
2
+ import { MAX_ARRAY_FIELD_VALUES, } from "./model.js";
3
+ const buildAqlFromFieldFilter = (field) => {
4
+ const { value: fieldValue } = field;
5
+ const { key, type, strict, value } = fieldValue;
6
+ if (type === "array") {
7
+ if (value.length === 0) {
8
+ throw new Error("ArrayField value cannot be empty");
9
+ }
10
+ if (strict === false) {
11
+ return buildArrayIntersectionFilter(key, value, MAX_ARRAY_FIELD_VALUES);
12
+ }
13
+ return aqlArrayConditionExpression({
14
+ type: "arrayCondition",
15
+ left: {
16
+ identifier: key,
17
+ },
18
+ operator: "IN",
19
+ right: value.map((item) => ({
20
+ value: item,
21
+ type: "STRING",
22
+ })),
23
+ });
24
+ }
25
+ let expressionValue;
26
+ let valueType;
27
+ let operator = "EQ";
28
+ switch (type) {
29
+ case "number": {
30
+ expressionValue = String(value);
31
+ operator = (strict ?? true) ? "EQ" : "CONTAINS";
32
+ valueType = "NUMBER";
33
+ break;
34
+ }
35
+ case "boolean": {
36
+ expressionValue = value ? "true" : "false";
37
+ valueType = "BOOLEAN";
38
+ operator = "EQ";
39
+ break;
40
+ }
41
+ case "string": {
42
+ expressionValue = String(value);
43
+ operator = (strict ?? true) ? "EQ" : "CONTAINS";
44
+ valueType = "STRING";
45
+ break;
46
+ }
47
+ default: {
48
+ const exhaustiveCheck = type;
49
+ throw new Error(`Unsupported field type: ${String(exhaustiveCheck)}`);
50
+ }
51
+ }
52
+ return aqlConditionExpression({
53
+ type: "condition",
54
+ left: {
55
+ identifier: key,
56
+ },
57
+ operator,
58
+ right: {
59
+ value: expressionValue,
60
+ type: valueType,
61
+ },
62
+ });
63
+ };
64
+ export const buildFieldFilters = (filters) => {
65
+ if (filters.length === 0) {
66
+ throw new Error("chainFieldFilters: filters array cannot be empty");
67
+ }
68
+ const buildAqlFromFilterGroup = (group) => {
69
+ const { value } = group;
70
+ if (value.length === 0) {
71
+ throw new Error("buildFieldFilters: value array cannot be empty");
72
+ }
73
+ if (value.length === 1) {
74
+ return buildFieldFilters([value[0]]);
75
+ }
76
+ return aqlParenExpression({
77
+ type: "paren",
78
+ expression: buildFieldFilters(value),
79
+ });
80
+ };
81
+ if (filters.length === 1) {
82
+ const [filter] = filters;
83
+ if (filter.type === "field") {
84
+ return buildAqlFromFieldFilter(filter);
85
+ }
86
+ return buildAqlFromFilterGroup(filter);
87
+ }
88
+ const [first, second, ...rest] = filters;
89
+ if (rest.length === 0) {
90
+ return aqlBinaryExpression({
91
+ type: "binary",
92
+ operator: first.logicalOperator ?? "AND",
93
+ left: buildFieldFilters([first]),
94
+ right: buildFieldFilters([second]),
95
+ });
96
+ }
97
+ return rest.reduce((acc, filter) => {
98
+ return aqlBinaryExpression({
99
+ type: "binary",
100
+ operator: filter.logicalOperator ?? "AND",
101
+ left: acc,
102
+ right: buildFieldFilters([filter]),
103
+ });
104
+ }, buildFieldFilters([first, second]));
105
+ };
106
+ const buildArrayIntersectionFilter = (key, values, maxIndex = 20) => {
107
+ if (values.length === 0) {
108
+ throw new Error("buildArrayIntersectionFilter: values array cannot be empty");
109
+ }
110
+ const conditionsPerValue = values.map((value) => {
111
+ const indexConditions = Array.from({ length: maxIndex + 1 }, (_, index) => {
112
+ return aqlConditionExpression({
113
+ type: "condition",
114
+ left: {
115
+ identifier: key,
116
+ param: {
117
+ value: index,
118
+ type: "number",
119
+ },
120
+ },
121
+ operator: "EQ",
122
+ right: {
123
+ value,
124
+ type: "STRING",
125
+ },
126
+ });
127
+ });
128
+ if (indexConditions.length === 1) {
129
+ return indexConditions[0];
130
+ }
131
+ return indexConditions.reduce((acc, condition, index) => {
132
+ if (index === 0) {
133
+ return condition;
134
+ }
135
+ return aqlBinaryExpression({
136
+ type: "binary",
137
+ operator: "OR",
138
+ left: acc,
139
+ right: condition,
140
+ });
141
+ }, indexConditions[0]);
142
+ });
143
+ if (conditionsPerValue.length === 1) {
144
+ return conditionsPerValue[0];
145
+ }
146
+ return conditionsPerValue.reduce((acc, condition, index) => {
147
+ if (index === 0) {
148
+ return condition;
149
+ }
150
+ return aqlBinaryExpression({
151
+ type: "binary",
152
+ operator: "OR",
153
+ left: acc,
154
+ right: condition,
155
+ });
156
+ }, conditionsPerValue[0]);
157
+ };
158
+ export const buildFilterPredicate = (filters) => {
159
+ return createAqlPredicate(buildFieldFilters(filters));
160
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./builders.js";
2
+ export type { ArrayField, BooleanField, Field, FieldFilter, FieldFilterGroup, Filter, LogicalOperator, NumberField, StringField, } from "./model.js";
3
+ export { MAX_ARRAY_FIELD_VALUES } from "./model.js";
@@ -0,0 +1,2 @@
1
+ export * from "./builders.js";
2
+ export { MAX_ARRAY_FIELD_VALUES } from "./model.js";
@@ -0,0 +1,39 @@
1
+ export type FieldFilter<T extends string = string> = {
2
+ type: "field";
3
+ value: Field<T>;
4
+ logicalOperator?: LogicalOperator;
5
+ };
6
+ export type FieldFilterGroup<T extends string = string> = {
7
+ type: "group";
8
+ value: Filter<T>[];
9
+ logicalOperator?: LogicalOperator;
10
+ };
11
+ export type Filter<T extends string = string> = FieldFilter<T> | FieldFilterGroup<T>;
12
+ export type StringField<T extends string = string> = {
13
+ key: T;
14
+ value: string;
15
+ strict?: boolean;
16
+ type: "string";
17
+ };
18
+ export type NumberField<T extends string = string> = {
19
+ key: T;
20
+ value: number;
21
+ strict?: boolean;
22
+ type: "number";
23
+ };
24
+ export type BooleanField<T extends string = string> = {
25
+ key: T;
26
+ value: boolean;
27
+ strict?: never;
28
+ type: "boolean";
29
+ };
30
+ export type ArrayField<T extends string = string> = {
31
+ key: T;
32
+ value: string[];
33
+ strict?: boolean;
34
+ type: "array";
35
+ };
36
+ export type Field<T extends string = string> = StringField<T> | NumberField<T> | BooleanField<T> | ArrayField<T>;
37
+ export type LogicalOperator = "AND" | "OR";
38
+ export type AqlValueType = "STRING" | "NUMBER" | "BOOLEAN";
39
+ export declare const MAX_ARRAY_FIELD_VALUES = 20;
@@ -0,0 +1 @@
1
+ export const MAX_ARRAY_FIELD_VALUES = 20;
package/dist/index.d.ts CHANGED
@@ -4,3 +4,10 @@ export * from "./i18n.js";
4
4
  export * from "./charts/index.js";
5
5
  export * from "./strings.js";
6
6
  export * from "./sanitize.js";
7
+ export * from "./stores/url/index.js";
8
+ export * from "./stores/router/index.js";
9
+ export * from "./filters/index.js";
10
+ export * from "./stores/theme/index.js";
11
+ export * from "./stores/loadableStore/index.js";
12
+ export * from "./stores/persister/index.js";
13
+ export * from "./utils.js";
package/dist/index.js CHANGED
@@ -4,3 +4,10 @@ export * from "./i18n.js";
4
4
  export * from "./charts/index.js";
5
5
  export * from "./strings.js";
6
6
  export * from "./sanitize.js";
7
+ export * from "./stores/url/index.js";
8
+ export * from "./stores/router/index.js";
9
+ export * from "./filters/index.js";
10
+ export * from "./stores/theme/index.js";
11
+ export * from "./stores/loadableStore/index.js";
12
+ export * from "./stores/persister/index.js";
13
+ export * from "./utils.js";
@@ -0,0 +1 @@
1
+ export declare const LOADABLE_STORE_BRAND: unique symbol;
@@ -0,0 +1 @@
1
+ export const LOADABLE_STORE_BRAND = Symbol("loadableStore");
@@ -0,0 +1,3 @@
1
+ export { loadableStore, type LoadableStore } from "./store.js";
2
+ export type { LoadableStoreValue } from "./types.js";
3
+ export { isLoadableStore } from "./utils.js";
@@ -0,0 +1,2 @@
1
+ export { loadableStore } from "./store.js";
2
+ export { isLoadableStore } from "./utils.js";
@@ -0,0 +1,13 @@
1
+ import { LOADABLE_STORE_BRAND } from "./constants.js";
2
+ import type { LoadableStoreValue } from "./types.js";
3
+ export type LoadableStore<T> = ReturnType<typeof loadableStore<T>>;
4
+ export declare const loadableStore: <T>(options: {
5
+ initialState: T;
6
+ }) => {
7
+ readonly value: LoadableStoreValue<T>;
8
+ readonly onInit: () => void;
9
+ readonly onLoad: (silent?: boolean) => void;
10
+ readonly onSuccess: (data: T) => void;
11
+ readonly onError: (error?: Error) => void;
12
+ readonly brand: typeof LOADABLE_STORE_BRAND;
13
+ };
@@ -0,0 +1,47 @@
1
+ import { batch, computed, signal } from "@preact/signals-core";
2
+ import { LOADABLE_STORE_BRAND } from "./constants.js";
3
+ export const loadableStore = (options) => {
4
+ const loadingSignal = signal(false);
5
+ const errorSignal = signal(undefined);
6
+ const dataSignal = signal(options.initialState);
7
+ return {
8
+ value: {
9
+ data: computed(() => dataSignal.value),
10
+ loading: computed(() => loadingSignal.value),
11
+ error: computed(() => errorSignal.value),
12
+ errorMessage: computed(() => {
13
+ if (errorSignal.value) {
14
+ return errorSignal.value.message;
15
+ }
16
+ return undefined;
17
+ }),
18
+ },
19
+ onInit: () => {
20
+ batch(() => {
21
+ loadingSignal.value = false;
22
+ errorSignal.value = undefined;
23
+ dataSignal.value = options.initialState;
24
+ });
25
+ },
26
+ onLoad: (silent = false) => {
27
+ batch(() => {
28
+ loadingSignal.value = !silent;
29
+ errorSignal.value = undefined;
30
+ });
31
+ },
32
+ onSuccess: (data) => {
33
+ batch(() => {
34
+ loadingSignal.value = false;
35
+ errorSignal.value = undefined;
36
+ dataSignal.value = data;
37
+ });
38
+ },
39
+ onError: (error = new Error("Unknown error")) => {
40
+ batch(() => {
41
+ loadingSignal.value = false;
42
+ errorSignal.value = error;
43
+ });
44
+ },
45
+ brand: LOADABLE_STORE_BRAND,
46
+ };
47
+ };
@@ -0,0 +1,7 @@
1
+ import type { ReadonlySignal } from "@preact/signals-core";
2
+ export type LoadableStoreValue<T> = {
3
+ error: ReadonlySignal<Error | undefined>;
4
+ errorMessage: ReadonlySignal<string | undefined>;
5
+ loading: ReadonlySignal<boolean>;
6
+ data: ReadonlySignal<T>;
7
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { LoadableStore } from "./store.js";
2
+ export declare const isLoadableStore: <T>(value: unknown) => value is LoadableStore<T>;
@@ -0,0 +1,7 @@
1
+ import { LOADABLE_STORE_BRAND } from "./constants.js";
2
+ export const isLoadableStore = (value) => {
3
+ if (typeof value !== "object" || value === null) {
4
+ return false;
5
+ }
6
+ return "brand" in value && value.brand === LOADABLE_STORE_BRAND;
7
+ };
@@ -0,0 +1,12 @@
1
+ import { type Signal } from "@preact/signals-core";
2
+ export declare const persistSignal: <S extends Signal<unknown>>(options: {
3
+ signal: S;
4
+ key: string;
5
+ shouldPersist?: (v: S extends Signal<infer U> ? U : never) => boolean;
6
+ }) => (() => void) | undefined;
7
+ export declare const restoreSignal: <S extends Signal<unknown>, V = S extends Signal<infer U> ? U : unknown>(options: {
8
+ signal: S;
9
+ key: string;
10
+ defaultValue?: V;
11
+ onRestore?: (value: any) => V | undefined;
12
+ }) => void;
@@ -0,0 +1,36 @@
1
+ import { effect } from "@preact/signals-core";
2
+ export const persistSignal = (options) => {
3
+ if (typeof window === "undefined") {
4
+ return;
5
+ }
6
+ const { signal, key, shouldPersist = () => true } = options;
7
+ return effect(() => {
8
+ const value = signal.value;
9
+ if (!shouldPersist(value)) {
10
+ return;
11
+ }
12
+ try {
13
+ window.localStorage.setItem(key, typeof value === "string" ? value : JSON.stringify(value));
14
+ }
15
+ catch { }
16
+ });
17
+ };
18
+ export const restoreSignal = (options) => {
19
+ if (typeof window === "undefined") {
20
+ return;
21
+ }
22
+ const { signal, key, defaultValue, onRestore = (v) => v } = options;
23
+ const value = window.localStorage.getItem(key);
24
+ if (value !== null) {
25
+ let parsedValue = value;
26
+ try {
27
+ parsedValue = JSON.parse(value);
28
+ }
29
+ catch { }
30
+ signal.value = onRestore(parsedValue);
31
+ return;
32
+ }
33
+ if ("defaultValue" in options) {
34
+ signal.value = defaultValue;
35
+ }
36
+ };
@@ -0,0 +1,18 @@
1
+ export declare const router: import("@preact/signals-core").ReadonlySignal<{
2
+ readonly path: string;
3
+ readonly pathParts: string[];
4
+ }>;
5
+ type NavigateTo = {
6
+ path: string;
7
+ keepSearchParams?: boolean;
8
+ searchParams?: Record<string, string | string[] | number | undefined>;
9
+ params?: Record<string, string | undefined>;
10
+ replace?: boolean;
11
+ };
12
+ export declare const navigateTo: (to: NavigateTo) => void;
13
+ type Route<Params extends Record<string, string | undefined>> = {
14
+ matches: boolean;
15
+ params: Params;
16
+ };
17
+ export declare const createRoute: <Params extends Record<string, string | undefined>>(path: string, validate?: (route: Route<Params>) => boolean) => Route<Params>;
18
+ export {};
@@ -0,0 +1,95 @@
1
+ import { computed } from "@preact/signals-core";
2
+ import { paramsToSearchParams } from "../url/helpers.js";
3
+ import { currentUrl, goTo } from "../url/index.js";
4
+ export const router = computed(() => {
5
+ const hash = currentUrl.value.hash.startsWith("#") ? currentUrl.value.hash.slice(1) : currentUrl.value.hash;
6
+ return {
7
+ path: hash,
8
+ pathParts: hash.split("/").filter(Boolean),
9
+ };
10
+ });
11
+ const createRouteUrl = (path, params) => {
12
+ return path
13
+ .split("/")
14
+ .map((part) => {
15
+ if (part.startsWith(":")) {
16
+ const isOptional = part.endsWith("?");
17
+ const paramKey = isOptional ? part.slice(1, -1) : part.slice(1);
18
+ const value = params[paramKey];
19
+ if (value) {
20
+ return value.toString();
21
+ }
22
+ if (isOptional) {
23
+ return "";
24
+ }
25
+ return part;
26
+ }
27
+ return part;
28
+ })
29
+ .filter(Boolean)
30
+ .join("/");
31
+ };
32
+ export const navigateTo = (to) => {
33
+ const { path, params = {}, replace = false, searchParams = {}, keepSearchParams = false } = to;
34
+ const currentPathname = currentUrl.value.pathname;
35
+ const newUrl = new URL(currentPathname, currentUrl.value.origin);
36
+ const routeUrl = createRouteUrl(path, params);
37
+ newUrl.hash = routeUrl === "" || routeUrl === "/" ? "" : `#${routeUrl}`;
38
+ if (keepSearchParams) {
39
+ paramsToSearchParams(currentUrl.value.params, newUrl.searchParams);
40
+ }
41
+ Object.entries(searchParams).forEach(([key, value]) => {
42
+ if (!value) {
43
+ return;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ for (const v of value) {
47
+ newUrl.searchParams.set(key, v.toString());
48
+ }
49
+ }
50
+ newUrl.searchParams.set(key, value.toString());
51
+ });
52
+ goTo(newUrl, { replace });
53
+ };
54
+ export const createRoute = (path, validate = () => true) => {
55
+ if (path === "/" && router.value.pathParts.length === 0) {
56
+ return { matches: true, params: {} };
57
+ }
58
+ const routeParts = path.split("/").filter(Boolean);
59
+ const currentParts = router.value.pathParts;
60
+ const params = {};
61
+ let routeIndex = 0;
62
+ let currentIndex = 0;
63
+ while (routeIndex < routeParts.length && currentIndex < currentParts.length) {
64
+ const routePart = routeParts[routeIndex];
65
+ const currentPart = currentParts[currentIndex];
66
+ if (routePart.startsWith(":")) {
67
+ const isOptional = routePart.endsWith("?");
68
+ const paramKey = isOptional ? routePart.slice(1, -1) : routePart.slice(1);
69
+ params[paramKey] = currentPart;
70
+ routeIndex++;
71
+ currentIndex++;
72
+ }
73
+ else if (routePart === currentPart) {
74
+ routeIndex++;
75
+ currentIndex++;
76
+ }
77
+ else {
78
+ return { matches: false, params: {} };
79
+ }
80
+ }
81
+ while (routeIndex < routeParts.length) {
82
+ const routePart = routeParts[routeIndex];
83
+ if (routePart.startsWith(":") && routePart.endsWith("?")) {
84
+ const paramKey = routePart.slice(1, -1);
85
+ params[paramKey] = undefined;
86
+ routeIndex++;
87
+ }
88
+ else {
89
+ return { matches: false, params: {} };
90
+ }
91
+ }
92
+ const matches = currentIndex === currentParts.length;
93
+ const route = { matches, params };
94
+ return { matches: matches && validate(route), params };
95
+ };
@@ -0,0 +1,3 @@
1
+ import type { UserTheme } from "./types.js";
2
+ export declare const setUserTheme: (theme: UserTheme) => void;
3
+ export declare const toggleUserTheme: () => void;
@@ -0,0 +1,30 @@
1
+ import { effect } from "@preact/signals-core";
2
+ import { SELECTED_THEMES, THEME_DARK, THEME_LIGHT } from "./constants.js";
3
+ import { currentTheme, preferredTheme, userTheme } from "./store.js";
4
+ import { getPrefersColorSchemeMQ } from "./utils.js";
5
+ const initThemeStore = () => {
6
+ if (typeof window === "undefined") {
7
+ return;
8
+ }
9
+ effect(() => {
10
+ document.documentElement.setAttribute("data-theme", currentTheme.value);
11
+ });
12
+ const preffersMediaQuery = getPrefersColorSchemeMQ();
13
+ preffersMediaQuery.addEventListener("change", (event) => {
14
+ if (event.matches) {
15
+ preferredTheme.value = THEME_DARK;
16
+ }
17
+ else {
18
+ preferredTheme.value = THEME_LIGHT;
19
+ }
20
+ });
21
+ };
22
+ export const setUserTheme = (theme) => {
23
+ userTheme.value = theme;
24
+ };
25
+ export const toggleUserTheme = () => {
26
+ const current = userTheme.peek();
27
+ const next = SELECTED_THEMES[(SELECTED_THEMES.indexOf(current) + 1) % SELECTED_THEMES.length];
28
+ setUserTheme(next);
29
+ };
30
+ initThemeStore();
@@ -0,0 +1,6 @@
1
+ import type { UserTheme } from "./types.js";
2
+ export declare const THEME_AUTO = "auto";
3
+ export declare const THEME_LIGHT = "light";
4
+ export declare const THEME_DARK = "dark";
5
+ export declare const STORAGE_KEY = "theme";
6
+ export declare const SELECTED_THEMES: UserTheme[];
@@ -0,0 +1,5 @@
1
+ export const THEME_AUTO = "auto";
2
+ export const THEME_LIGHT = "light";
3
+ export const THEME_DARK = "dark";
4
+ export const STORAGE_KEY = "theme";
5
+ export const SELECTED_THEMES = [THEME_LIGHT, THEME_DARK, THEME_AUTO];
@@ -0,0 +1,2 @@
1
+ export { setUserTheme, toggleUserTheme } from "./actions.js";
2
+ export { themeStore } from "./store.js";
@@ -0,0 +1,2 @@
1
+ export { setUserTheme, toggleUserTheme } from "./actions.js";
2
+ export { themeStore } from "./store.js";
@@ -0,0 +1,8 @@
1
+ import type { UITheme, UserTheme } from "./types.js";
2
+ export declare const currentTheme: import("@preact/signals-core").ReadonlySignal<UITheme>;
3
+ export declare const userTheme: import("@preact/signals-core").Signal<UserTheme>;
4
+ export declare const preferredTheme: import("@preact/signals-core").Signal<UITheme>;
5
+ export declare const themeStore: import("@preact/signals-core").ReadonlySignal<{
6
+ readonly current: UITheme;
7
+ readonly selected: UserTheme;
8
+ }>;
@@ -0,0 +1,41 @@
1
+ import { computed, signal } from "@preact/signals-core";
2
+ import { getReportOptions } from "../../data.js";
3
+ import { persistSignal, restoreSignal } from "../persister/index.js";
4
+ import { STORAGE_KEY, THEME_AUTO, THEME_DARK, THEME_LIGHT } from "./constants.js";
5
+ import { getPrefersColorSchemeMQ, isAcceptedThemeValue, isAutoTheme } from "./utils.js";
6
+ const reportConfigTheme = getReportOptions()?.theme;
7
+ const getInitialPreferredTheme = () => {
8
+ if (typeof window === "undefined") {
9
+ return THEME_LIGHT;
10
+ }
11
+ if (getPrefersColorSchemeMQ().matches) {
12
+ return THEME_DARK;
13
+ }
14
+ return THEME_LIGHT;
15
+ };
16
+ export const currentTheme = computed(() => {
17
+ if (isAutoTheme(userTheme.value)) {
18
+ return preferredTheme.value;
19
+ }
20
+ return userTheme.value;
21
+ });
22
+ export const userTheme = signal(THEME_AUTO);
23
+ export const preferredTheme = signal(getInitialPreferredTheme());
24
+ restoreSignal({
25
+ signal: userTheme,
26
+ key: STORAGE_KEY,
27
+ onRestore: (value) => {
28
+ if (isAcceptedThemeValue(value)) {
29
+ return value;
30
+ }
31
+ return reportConfigTheme ?? THEME_AUTO;
32
+ },
33
+ });
34
+ persistSignal({
35
+ signal: userTheme,
36
+ key: STORAGE_KEY,
37
+ });
38
+ export const themeStore = computed(() => ({
39
+ current: currentTheme.value,
40
+ selected: userTheme.value,
41
+ }));
@@ -0,0 +1,2 @@
1
+ export type UITheme = "light" | "dark";
2
+ export type UserTheme = UITheme | "auto";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { UserTheme } from "./types.js";
2
+ export declare const getPrefersColorSchemeMQ: () => MediaQueryList;
3
+ export declare const isAcceptedThemeValue: (value: unknown) => value is UserTheme;
4
+ export declare const isAutoTheme: (theme: UserTheme) => theme is "auto";
@@ -0,0 +1,23 @@
1
+ import { SELECTED_THEMES, THEME_AUTO } from "./constants.js";
2
+ const nullMediaQueryList = {
3
+ matches: false,
4
+ addEventListener: () => { },
5
+ removeEventListener: () => { },
6
+ media: "",
7
+ onchange: () => { },
8
+ addListener: () => { },
9
+ removeListener: () => { },
10
+ dispatchEvent: () => true,
11
+ };
12
+ export const getPrefersColorSchemeMQ = () => {
13
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
14
+ return nullMediaQueryList;
15
+ }
16
+ return window.matchMedia("(prefers-color-scheme: dark)");
17
+ };
18
+ export const isAcceptedThemeValue = (value) => {
19
+ return SELECTED_THEMES.includes(value);
20
+ };
21
+ export const isAutoTheme = (theme) => {
22
+ return theme === THEME_AUTO;
23
+ };
@@ -0,0 +1,12 @@
1
+ export declare const subscribeToUrlChange: (callback: () => void) => (() => void) | undefined;
2
+ type NavigateTo = URL | string | {
3
+ path: string;
4
+ };
5
+ type NavigateToOptions = {
6
+ replace?: boolean;
7
+ };
8
+ export declare const goTo: (to: NavigateTo, options?: NavigateToOptions) => void;
9
+ export declare const getCurrentUrl: () => string;
10
+ export declare const paramsToSearchParams: (params: Record<string, string | string[]>, searchParams?: URLSearchParams) => URLSearchParams;
11
+ export declare const searchParamsToParams: (searchParams: URLSearchParams) => Record<string, string | string[]>;
12
+ export {};
@@ -0,0 +1,74 @@
1
+ export const subscribeToUrlChange = (callback) => {
2
+ if (typeof window === "undefined") {
3
+ return;
4
+ }
5
+ window.addEventListener("popstate", callback);
6
+ window.addEventListener("replaceState", callback);
7
+ window.addEventListener("pushState", callback);
8
+ window.addEventListener("hashchange", callback);
9
+ return () => {
10
+ window.removeEventListener("popstate", callback);
11
+ window.removeEventListener("replaceState", callback);
12
+ window.removeEventListener("pushState", callback);
13
+ window.removeEventListener("hashchange", callback);
14
+ };
15
+ };
16
+ const getUrl = (to) => {
17
+ if (typeof to === "string") {
18
+ return to;
19
+ }
20
+ if (to instanceof URL) {
21
+ return to;
22
+ }
23
+ return new URL(to.path, getCurrentUrl());
24
+ };
25
+ export const goTo = (to, options) => {
26
+ if (typeof window === "undefined") {
27
+ return;
28
+ }
29
+ const url = getUrl(to);
30
+ if (options?.replace) {
31
+ window.history.replaceState(null, "", url);
32
+ window.dispatchEvent(new Event("replaceState"));
33
+ }
34
+ else {
35
+ window.history.pushState(null, "", url);
36
+ window.dispatchEvent(new Event("pushState"));
37
+ }
38
+ };
39
+ export const getCurrentUrl = () => {
40
+ if (typeof window === "undefined") {
41
+ return "";
42
+ }
43
+ return window.location.href;
44
+ };
45
+ export const paramsToSearchParams = (params, searchParams = new URLSearchParams()) => {
46
+ Object.entries(params).forEach(([key, value]) => {
47
+ if (Array.isArray(value)) {
48
+ for (const v of value) {
49
+ searchParams.append(key, v);
50
+ }
51
+ }
52
+ else {
53
+ searchParams.set(key, value);
54
+ }
55
+ });
56
+ return searchParams;
57
+ };
58
+ export const searchParamsToParams = (searchParams) => {
59
+ const params = {};
60
+ searchParams.forEach((value, key) => {
61
+ if (key in params) {
62
+ if (Array.isArray(params[key])) {
63
+ params[key].push(value);
64
+ }
65
+ else {
66
+ params[key] = [params[key], value];
67
+ }
68
+ }
69
+ else {
70
+ params[key] = value;
71
+ }
72
+ });
73
+ return params;
74
+ };
@@ -0,0 +1,2 @@
1
+ export { getParamValue, getParamValues, hasParam, setParams, currentUrl, type Param } from "./store.js";
2
+ export { goTo, getCurrentUrl } from "./helpers.js";
@@ -0,0 +1,2 @@
1
+ export { getParamValue, getParamValues, hasParam, setParams, currentUrl } from "./store.js";
2
+ export { goTo, getCurrentUrl } from "./helpers.js";
@@ -0,0 +1,19 @@
1
+ export declare const currentUrlSignal: import("@preact/signals-core").Signal<string>;
2
+ export declare const urlSearchParams: import("@preact/signals-core").ReadonlySignal<URLSearchParams>;
3
+ export type Param = {
4
+ key: string;
5
+ value: string | string[] | undefined;
6
+ };
7
+ export declare const setParams: (...params: Param[]) => void;
8
+ export declare const currentUrl: import("@preact/signals-core").ReadonlySignal<{
9
+ readonly hash: string;
10
+ readonly pathname: string;
11
+ readonly origin: string;
12
+ readonly params: Record<string, string | string[]>;
13
+ }>;
14
+ export declare const getParamValue: (key: string) => string | null;
15
+ export declare const getParamValues: (key: string) => string[];
16
+ export declare const hasParam: (key: string) => boolean;
17
+ export declare const getParamValueComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<string | null>;
18
+ export declare const getParamValuesComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<string[]>;
19
+ export declare const hasParamComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<boolean>;
@@ -0,0 +1,45 @@
1
+ import { computed, signal } from "@preact/signals-core";
2
+ import { getCurrentUrl, goTo, searchParamsToParams, subscribeToUrlChange } from "./helpers.js";
3
+ export const currentUrlSignal = signal(getCurrentUrl());
4
+ subscribeToUrlChange(() => {
5
+ if (currentUrlSignal.peek() === getCurrentUrl()) {
6
+ return;
7
+ }
8
+ currentUrlSignal.value = getCurrentUrl();
9
+ });
10
+ const urlSignal = computed(() => new URL(currentUrlSignal.value));
11
+ export const urlSearchParams = computed(() => urlSignal.value.searchParams);
12
+ export const setParams = (...params) => {
13
+ const newUrl = new URL(getCurrentUrl());
14
+ for (const param of params) {
15
+ newUrl.searchParams.delete(param.key);
16
+ if (Array.isArray(param.value)) {
17
+ for (const value of param.value) {
18
+ newUrl.searchParams.append(param.key, value);
19
+ }
20
+ }
21
+ else if (typeof param.value === "string") {
22
+ newUrl.searchParams.set(param.key, param.value);
23
+ }
24
+ }
25
+ if (newUrl.href === urlSignal.peek().href) {
26
+ return;
27
+ }
28
+ goTo(newUrl.href, {
29
+ replace: true,
30
+ });
31
+ };
32
+ export const currentUrl = computed(() => {
33
+ return {
34
+ hash: urlSignal.value.hash,
35
+ pathname: urlSignal.value.pathname,
36
+ origin: urlSignal.value.origin,
37
+ params: searchParamsToParams(urlSearchParams.value),
38
+ };
39
+ });
40
+ export const getParamValue = (key) => urlSearchParams.value.get(key);
41
+ export const getParamValues = (key) => urlSearchParams.value.getAll(key);
42
+ export const hasParam = (key) => urlSearchParams.value.has(key);
43
+ export const getParamValueComputed = (key) => computed(() => getParamValue(key));
44
+ export const getParamValuesComputed = (key) => computed(() => getParamValues(key));
45
+ export const hasParamComputed = (key) => computed(() => hasParam(key));
@@ -0,0 +1,2 @@
1
+ import { type ReadonlySignal, type Signal } from "@preact/signals-core";
2
+ export declare const isSignal: (value: unknown) => value is Signal | ReadonlySignal;
package/dist/utils.js ADDED
@@ -0,0 +1,9 @@
1
+ import { signal } from "@preact/signals-core";
2
+ const signalExample = signal(1);
3
+ export const isSignal = (value) => {
4
+ return (typeof value === "object" &&
5
+ value !== null &&
6
+ "brand" in value &&
7
+ typeof value.brand === "symbol" &&
8
+ value.brand === signalExample.brand);
9
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/web-commons",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Collection of utilities used across the web Allure reports",
5
5
  "keywords": [
6
6
  "allure",
@@ -20,12 +20,16 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "build": "run clean && tsc --project ./tsconfig.json",
23
- "clean": "rimraf ./dist"
23
+ "clean": "rimraf ./dist",
24
+ "test": "vitest run"
24
25
  },
25
26
  "dependencies": {
26
- "@allurereport/charts-api": "3.0.1",
27
- "@allurereport/core-api": "3.0.1",
28
- "@allurereport/plugin-api": "3.0.1",
27
+ "@allurereport/aql": "3.1.0",
28
+ "@allurereport/charts-api": "3.1.0",
29
+ "@allurereport/core-api": "3.1.0",
30
+ "@allurereport/plugin-api": "3.1.0",
31
+ "@preact/signals": "^2.6.1",
32
+ "@preact/signals-core": "^1.12.2",
29
33
  "ansi-to-html": "^0.7.2",
30
34
  "d3-interpolate": "^3.0.1",
31
35
  "d3-scale": "^4.0.2",
@@ -51,6 +55,7 @@
51
55
  "eslint-plugin-n": "^17.10.1",
52
56
  "eslint-plugin-no-null": "^1.0.2",
53
57
  "eslint-plugin-prefer-arrow": "^1.2.3",
58
+ "jsdom": "^26.1.0",
54
59
  "rimraf": "^6.0.1",
55
60
  "tslib": "^2.7.0",
56
61
  "typescript": "^5.6.3",