parent
e3266ffac4
commit
88ec6b6f2b
@ -0,0 +1,102 @@
|
|||||||
|
# ImageAdjustments — auditoría de calidad (2026-09-26)
|
||||||
|
|
||||||
|
## Alcance y pre-flight
|
||||||
|
|
||||||
|
Se revisan Morfo, Soma, Sema y Eidos, los cinco wrappers headless, catálogo,
|
||||||
|
receta, traducciones y consumidores ImagePicker/Palabras. Se han leído completos
|
||||||
|
component-audit, demo-authoring, el contrato Eidos y building-a-component.
|
||||||
|
Alcance autorizado por el usuario en esta auditoría: caracterización, duplicación
|
||||||
|
y refactorización neutral; los defectos funcionales quedan para después.
|
||||||
|
No se modifica web. La ficha histórica de julio era de análisis, no cobertura
|
||||||
|
nativa; no se reabre la corrección de value-color ya registrada como resuelta.
|
||||||
|
No hay componente equivalente en el árbol Air de la referencia morfo-runtime.
|
||||||
|
|
||||||
|
## Matriz de referencias
|
||||||
|
|
||||||
|
Las referencias son primitivas de slider; no se atribuye a esas bibliotecas
|
||||||
|
un editor de filtros inexistente por ausencia de un ejemplo en su página.
|
||||||
|
|
||||||
|
| Aspecto | [Ark UI](https://ark-ui.com/docs/components/slider) | [Radix](https://www.radix-ui.com/primitives/docs/components/slider) | [React Aria](https://react-aria.adobe.com/Slider) | [Mantine](https://mantine.dev/core/slider/) | UIX y decisión |
|
||||||
|
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------- |
|
||||||
|
| Nombre | Label y nombres por thumb | Composición y aria del control | Label y nombres de thumbs | thumbLabel | Contrastar los ocho thumbs; Q77 ya existe, no crear otro ID. |
|
||||||
|
| Teclado y límites | min/max/step | Flechas, Home/End y Page | minValue/maxValue/step | Paso de teclado | Probar entrada nativa contra rangos actuales, sin cambiar Slider. |
|
||||||
|
| Valor | Callbacks de cambio y fin | Controlado o default | value/onChange/onChangeEnd | value y formatter | Conservar record parcial y callbacks propios de UIX. |
|
||||||
|
| Read-out | Composición | Composición | SliderOutput | Texto del valor y marks | Conservar ItemValue sin añadir una live region. |
|
||||||
|
| Disabled | Prop de estado | Prop de estado | Estado del control | Prop de estado | Probar controles y reset actuales. |
|
||||||
|
|
||||||
|
El [APG Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) exige nombre
|
||||||
|
para cada slider. [Filter Effects](https://www.w3.org/TR/filter-effects-1/)
|
||||||
|
define aplicación de funciones en el orden declarado. Estas fuentes orientan
|
||||||
|
la comprobación; no autorizan nuevas features ni cambios de semántica.
|
||||||
|
|
||||||
|
## Arquitectura y límites
|
||||||
|
|
||||||
|
Soma posee ocho definiciones, seis keys por defecto, values parciales,
|
||||||
|
computeImageFilter, isModified y reset. Morfo declara grupo/filas/reset y
|
||||||
|
commit-reset post; Sema aporta el tap y hereda el sonido del sistema.
|
||||||
|
Eidos compone un Slider por fila y un reset opcional; talla sm/md/lg viaja
|
||||||
|
al Slider, la tipografía de cabecera usa tokens propios sin eje de talla.
|
||||||
|
No hay variantes ni intención cromática pública. El catálogo aproxima
|
||||||
|
temperature; highlights/shadows y white balance exacto siguen diferidos.
|
||||||
|
|
||||||
|
ImagePicker aplica el filtro al contenedor Image. Palabras adapta valores a
|
||||||
|
su modelo; schemeImageFilter usa escalas, serialización y neutral distinto.
|
||||||
|
Esas semejanzas no justifican reemplazar sus funciones sin demostrar paridad.
|
||||||
|
No se crea una clase base para providers ni un constructor genérico de catálogo.
|
||||||
|
|
||||||
|
## Baseline y trabajo en curso
|
||||||
|
|
||||||
|
component:audit pasa con cero errores y un aviso D-1.2 de la demo congelada.
|
||||||
|
Los doce tests existentes han pasado en los lotes dirigidos de ImagePicker,
|
||||||
|
y el gate anterior pasa 505 archivos/5584 tests. La suite existente sustituye
|
||||||
|
Soma; se añade caracterización con servicios UIX reales antes de producción.
|
||||||
|
|
||||||
|
Diez canarios Eidos con servicios reales pasan junto a esa suite: **22/22 en
|
||||||
|
dos archivos**, antes de producción. Cada key se prueba por ArrowUp/Home/End,
|
||||||
|
rangos, read-out, callbacks value→filter y reset nativo. El grupo y el reset
|
||||||
|
usan nombres españoles. Se verifican disabled, talla de sliders sin cambiar
|
||||||
|
tipografía, reset opcional, orden del subconjunto, identidad keyed y retención
|
||||||
|
de values no visibles. Se inspeccionan capturas del panel completo y subconjunto.
|
||||||
|
Las capturas se solicitan con VITE_UIX_AUDIT_SCREENSHOTS=1; la suite normal
|
||||||
|
conserva acciones y aserciones sin recoger esos artefactos.
|
||||||
|
|
||||||
|
Ocho sondas adicionales de nombre fallan mientras pasan los 22 tests. Los
|
||||||
|
ocho thumbs reciben vacío frente a su label visible; se amplía **Q77**, sin
|
||||||
|
crear otro ID ni modificar Slider. Las sondas se retiran tras terminar.
|
||||||
|
No se acredita lector de pantalla, touch, wheel, otros motores, audio ni
|
||||||
|
vibración físicos.
|
||||||
|
|
||||||
|
## Extracción neutral y documentación
|
||||||
|
|
||||||
|
El wrapper root usa bindProps existente, con tipo derivado del parámetro
|
||||||
|
de ImageAdjustmentsProvider.create. Los seis enlaces conservan por AST nombres,
|
||||||
|
orden, getters y setters; el setter de value asigna y luego emite onValueChange
|
||||||
|
igual que antes. Se eliminan las llamadas repetidas de conexión sin crear
|
||||||
|
otro helper, interfaz ni export público. Baja de **50 a 49 líneas físicas**
|
||||||
|
y de **45 a 44 no vacías**: una línea menos. El catálogo, providers, CSS,
|
||||||
|
callbacks y exports ejecutables permanecen iguales. Item ahorraría muy poco
|
||||||
|
y no se extrae por simetría; Label/Value/Reset ya usan partOpts.
|
||||||
|
|
||||||
|
Las props externas actualizan rows y estado derivado, pero no emiten esos
|
||||||
|
callbacks por sí mismas; éstos salen de setAdjustment/reset. La caracterización
|
||||||
|
conserva ese comportamiento. Se documenta la diferencia entre estado derivado
|
||||||
|
y salida por callback, sin añadir un efecto que cambie la dinámica.
|
||||||
|
Se corrige la promesa documental de tipografía por size y el sonido fijo tick:
|
||||||
|
la talla afecta al Slider, la fila usa tokens y Sema hereda la selección sonora.
|
||||||
|
No se afirma ahorro de bundle o tiempo por reducir texto.
|
||||||
|
|
||||||
|
Después de la extracción pasan **25/25 tests en tres archivos**: las dos
|
||||||
|
suites de ajustes y opts-census. Se vuelven a inspeccionar ambas capturas.
|
||||||
|
La comprobación dirigida del helper pasa **7/7 en otro archivo**. El gate
|
||||||
|
completo pasa antes del ajuste de infraestructura visual: 506 archivos y
|
||||||
|
5594 tests, src/scripts sin errores de tipos y deuda de web dentro del ledger,
|
||||||
|
893 documentos sin errores ni avisos, build de apps/base y smoke 27/27 sin
|
||||||
|
errores de página, consola ni CSP.
|
||||||
|
|
||||||
|
T02 registra la escala reducida de las capturas por el iframe de Vitest.
|
||||||
|
La configuración audit-visual produce 1:1: **19/19 tests en cinco archivos**,
|
||||||
|
18 canarios de ZoomPan/Cropper/ImagePicker/ImageAdjustments y una sonda temporal
|
||||||
|
de escala. Se inspeccionaron ocho capturas nuevas y se retiró la sonda.
|
||||||
|
El gate con la configuración mantenida también pasa: 506/506 archivos y
|
||||||
|
5594/5594 tests, 893 documentos sin incidencias, src/scripts sin errores de
|
||||||
|
tipos y smoke apps/base 27/27. T02 queda cerrado como infraestructura visual.
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import { playwright } from '@vitest/browser-playwright';
|
||||||
|
import base from '../vite.config.ts';
|
||||||
|
|
||||||
|
const client = base.test?.projects?.find(
|
||||||
|
(project) =>
|
||||||
|
typeof project === 'object' &&
|
||||||
|
project !== null &&
|
||||||
|
'test' in project &&
|
||||||
|
project.test?.name === 'client'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof client !== 'object' || !client || !('test' in client) || !client.test?.browser) {
|
||||||
|
throw new Error('The client test project is required for UIX visual auditing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The larger outer viewport keeps the audit iframe at its native CSS scale.
|
||||||
|
// Reuse the normal plugins, aliases, instances and test discovery.
|
||||||
|
export default defineConfig({
|
||||||
|
...base,
|
||||||
|
test: {
|
||||||
|
name: 'audit-visual',
|
||||||
|
expect: base.test?.expect,
|
||||||
|
include: client.test.include,
|
||||||
|
browser: {
|
||||||
|
...client.test.browser,
|
||||||
|
ui: false,
|
||||||
|
provider: playwright({
|
||||||
|
contextOptions: { viewport: { width: 1280, height: 1400 } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
import { beforeAll, expect, it, vi } from 'vitest';
|
||||||
|
import { page, userEvent } from 'vitest/browser';
|
||||||
|
import { renderImageAdjustments } from './test/image-adjustments-audit-view';
|
||||||
|
|
||||||
|
beforeAll(() => userEvent.keyboard('{Shift}'), 60_000);
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
['brightness', 'Brillo', -100, 100, 1, '+1', 'brightness(1.01)'],
|
||||||
|
['contrast', 'Contraste', -100, 100, 1, '+1', 'contrast(1.01)'],
|
||||||
|
['saturation', 'Saturación', -100, 100, 1, '+1', 'saturate(1.01)'],
|
||||||
|
['temperature', 'Temperatura', -100, 100, 1, '+1', 'sepia(0.005)'],
|
||||||
|
['hue', 'Tono', -180, 180, 1, '1°', 'hue-rotate(1deg)'],
|
||||||
|
['blur', 'Desenfoque', 0, 20, 0.5, '0.5px', 'blur(0.5px)'],
|
||||||
|
['grayscale', 'Escala de grises', 0, 100, 1, '1%', 'grayscale(1%)'],
|
||||||
|
['sepia', 'Sepia', 0, 100, 1, '1%', 'sepia(1%)']
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
it.each(cases)(
|
||||||
|
'binds %s keyboard input, read-out, range and callback order',
|
||||||
|
async (key, label, min, max, step, formatted, filter) => {
|
||||||
|
const view = renderImageAdjustments();
|
||||||
|
try {
|
||||||
|
const row = view.root.querySelector<HTMLElement>(`[data-adjustment="${key}"]`)!;
|
||||||
|
const thumb = row.querySelector<HTMLElement>('[role="slider"]')!;
|
||||||
|
expect(row.querySelector('[data-image-adjustments-item-label]')!.textContent?.trim()).toBe(
|
||||||
|
label
|
||||||
|
);
|
||||||
|
expect(thumb.getAttribute('aria-valuemin')).toBe(String(min));
|
||||||
|
expect(thumb.getAttribute('aria-valuemax')).toBe(String(max));
|
||||||
|
thumb.focus();
|
||||||
|
await userEvent.keyboard('{ArrowUp}');
|
||||||
|
expect(view.value()).toEqual({ [key]: step });
|
||||||
|
expect(thumb.getAttribute('aria-valuenow')).toBe(String(step));
|
||||||
|
expect(row.querySelector('[data-image-adjustments-item-value]')!.textContent?.trim()).toBe(
|
||||||
|
formatted
|
||||||
|
);
|
||||||
|
expect(view.onValueChange).toHaveBeenLastCalledWith({ [key]: step });
|
||||||
|
expect(view.onFilterChange).toHaveBeenLastCalledWith(filter);
|
||||||
|
expect(view.events).toEqual(['value', 'filter']);
|
||||||
|
await userEvent.keyboard('{Home}');
|
||||||
|
expect(view.value()[key]).toBe(min);
|
||||||
|
await userEvent.keyboard('{End}');
|
||||||
|
expect(view.value()[key]).toBe(max);
|
||||||
|
const reset = view.root.querySelector<HTMLButtonElement>('[data-image-adjustments-reset]')!;
|
||||||
|
await expect.element(reset).toHaveAccessibleName('Restablecer');
|
||||||
|
await userEvent.click(reset);
|
||||||
|
await vi.waitFor(() => expect(view.value()).toEqual({}));
|
||||||
|
expect(view.onFilterChange).toHaveBeenLastCalledWith('none');
|
||||||
|
expect(view.events.slice(-2)).toEqual(['value', 'filter']);
|
||||||
|
expect(reset.disabled).toBe(true);
|
||||||
|
} finally {
|
||||||
|
await view.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
30_000
|
||||||
|
);
|
||||||
|
|
||||||
|
it('preserves keyed row identity, subset order and values outside the visible subset', async () => {
|
||||||
|
const view = renderImageAdjustments();
|
||||||
|
try {
|
||||||
|
await expect.element(view.root).toHaveAccessibleName('Ajustes de imagen');
|
||||||
|
await userEvent.click(view.button('Seed externally'));
|
||||||
|
await userEvent.click(view.button('Subset'));
|
||||||
|
expect(
|
||||||
|
Array.from(view.root.querySelectorAll('[data-adjustment]')).map((row) =>
|
||||||
|
row.getAttribute('data-adjustment')
|
||||||
|
)
|
||||||
|
).toEqual(['sepia', 'brightness']);
|
||||||
|
const brightness = view.root.querySelector<HTMLElement>('[data-adjustment="brightness"]')!;
|
||||||
|
const thumb = brightness.querySelector<HTMLElement>('[role="slider"]')!;
|
||||||
|
expect(thumb.getAttribute('aria-valuenow')).toBe('20');
|
||||||
|
await userEvent.click(view.button('Reverse subset'));
|
||||||
|
expect(view.root.querySelector('[data-adjustment="brightness"]')).toBe(brightness);
|
||||||
|
expect(
|
||||||
|
Array.from(view.root.querySelectorAll('[data-adjustment]')).map((row) =>
|
||||||
|
row.getAttribute('data-adjustment')
|
||||||
|
)
|
||||||
|
).toEqual(['brightness', 'sepia']);
|
||||||
|
thumb.focus();
|
||||||
|
await userEvent.keyboard('{ArrowUp}');
|
||||||
|
expect(view.value()).toEqual({ brightness: 21, contrast: -20, hue: 30 });
|
||||||
|
expect(view.onFilterChange).toHaveBeenLastCalledWith('brightness(1.21)');
|
||||||
|
expect(getComputedStyle(view.target.querySelector('[data-filter-preview]')!).filter).toBe(
|
||||||
|
'brightness(1.21)'
|
||||||
|
);
|
||||||
|
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
|
||||||
|
await page.screenshot({
|
||||||
|
element: view.target,
|
||||||
|
path: '../../../../../tmp/image-adjustments-subset.png'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await userEvent.click(view.root.querySelector('[data-image-adjustments-reset]')!);
|
||||||
|
await vi.waitFor(() => expect(view.value()).toEqual({}));
|
||||||
|
} finally {
|
||||||
|
await view.dispose();
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
|
it('propagates disabled, size and optional reset without changing row typography', async () => {
|
||||||
|
const view = renderImageAdjustments();
|
||||||
|
try {
|
||||||
|
const label = view.root.querySelector<HTMLElement>('[data-image-adjustments-item-label]')!;
|
||||||
|
const fontSize = getComputedStyle(label).fontSize;
|
||||||
|
const thumb = view.root.querySelector<HTMLElement>('[role="slider"]')!;
|
||||||
|
await userEvent.click(view.button('Seed externally'));
|
||||||
|
await userEvent.click(view.button('Toggle disabled'));
|
||||||
|
expect(view.onFilterChange).not.toHaveBeenCalled();
|
||||||
|
expect(view.root.hasAttribute('data-disabled')).toBe(true);
|
||||||
|
expect(thumb.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(
|
||||||
|
view.root.querySelector<HTMLButtonElement>('[data-image-adjustments-reset]')!.disabled
|
||||||
|
).toBe(true);
|
||||||
|
thumb.focus();
|
||||||
|
await userEvent.keyboard('{ArrowUp}');
|
||||||
|
expect(view.value().brightness).toBe(20);
|
||||||
|
expect(view.onValueChange).not.toHaveBeenCalled();
|
||||||
|
await userEvent.click(view.button('Toggle disabled'));
|
||||||
|
await userEvent.click(view.button('Toggle size'));
|
||||||
|
expect(view.root.getAttribute('data-size')).toBe('lg');
|
||||||
|
expect(
|
||||||
|
Array.from(view.root.querySelectorAll('[data-slider]')).every(
|
||||||
|
(slider) => slider.getAttribute('data-size') === 'lg'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(getComputedStyle(label).fontSize).toBe(fontSize);
|
||||||
|
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
|
||||||
|
await page.screenshot({
|
||||||
|
element: view.target,
|
||||||
|
path: '../../../../../tmp/image-adjustments-full.png'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await userEvent.click(view.button('Toggle reset'));
|
||||||
|
expect(view.root.querySelector('[data-image-adjustments-reset]')).toBeNull();
|
||||||
|
expect(view.root.querySelectorAll('[role="slider"]')).toHaveLength(8);
|
||||||
|
} finally {
|
||||||
|
await view.dispose();
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { setActiveUix, type ActiveUix } from '$active-uix';
|
||||||
|
import { Soma } from '$soma/core/soma.svelte';
|
||||||
|
import { ActiveEidos } from '$uix/eidos';
|
||||||
|
import ImageAdjustments from '../index';
|
||||||
|
import type { ImageAdjustmentKey, ImageAdjustmentValues, ImageAdjustmentsSize } from '../types';
|
||||||
|
|
||||||
|
let {
|
||||||
|
uix,
|
||||||
|
onValueChange,
|
||||||
|
onFilterChange
|
||||||
|
}: {
|
||||||
|
uix: ActiveUix;
|
||||||
|
onValueChange: (value: ImageAdjustmentValues) => void;
|
||||||
|
onFilterChange: (filter: string) => void;
|
||||||
|
} = $props();
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
setActiveUix(uix);
|
||||||
|
Soma.create();
|
||||||
|
const eidos = ActiveEidos.create({ applyDom: false });
|
||||||
|
onDestroy(() => eidos.dispose());
|
||||||
|
let value = $state<ImageAdjustmentValues>({});
|
||||||
|
let adjustments = $state<ImageAdjustmentKey[]>([
|
||||||
|
'brightness',
|
||||||
|
'contrast',
|
||||||
|
'saturation',
|
||||||
|
'temperature',
|
||||||
|
'hue',
|
||||||
|
'blur',
|
||||||
|
'grayscale',
|
||||||
|
'sepia'
|
||||||
|
]);
|
||||||
|
let disabled = $state(false);
|
||||||
|
let showReset = $state(true);
|
||||||
|
let size = $state<ImageAdjustmentsSize>('md');
|
||||||
|
let filter = $state('none');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section style="width: 360px; padding: 16px; background: white; color: black;" dir="rtl">
|
||||||
|
<h1>ImageAdjustments audit</h1>
|
||||||
|
<div
|
||||||
|
data-filter-preview
|
||||||
|
style:filter
|
||||||
|
style="height: 48px; background: linear-gradient(90deg, red, green, blue);"
|
||||||
|
></div>
|
||||||
|
<ImageAdjustments
|
||||||
|
bind:value
|
||||||
|
{adjustments}
|
||||||
|
{disabled}
|
||||||
|
{showReset}
|
||||||
|
{size}
|
||||||
|
{onValueChange}
|
||||||
|
onFilterChange={(next) => {
|
||||||
|
filter = next;
|
||||||
|
onFilterChange(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<output data-adjustments-value>{JSON.stringify(value)}</output>
|
||||||
|
<div>
|
||||||
|
<button onclick={() => (disabled = !disabled)}>Toggle disabled</button>
|
||||||
|
<button onclick={() => (showReset = !showReset)}>Toggle reset</button>
|
||||||
|
<button onclick={() => (size = size === 'md' ? 'lg' : 'md')}>Toggle size</button>
|
||||||
|
<button onclick={() => (adjustments = ['sepia', 'brightness'])}>Subset</button>
|
||||||
|
<button onclick={() => (adjustments = ['brightness', 'sepia'])}>Reverse subset</button>
|
||||||
|
<button onclick={() => (value = { brightness: 20, contrast: -20, hue: 30 })}
|
||||||
|
>Seed externally</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { flushSync, mount, unmount } from 'svelte';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import { createActiveUix } from '$active-uix';
|
||||||
|
import '../../../index.css';
|
||||||
|
import ImageAdjustmentsAuditHarness from './image-adjustments-audit-harness.svelte';
|
||||||
|
import type { ImageAdjustmentValues } from '../types';
|
||||||
|
|
||||||
|
export function renderImageAdjustments() {
|
||||||
|
const uix = createActiveUix({
|
||||||
|
langs: { schema: {}, defaultLocale: 'es' },
|
||||||
|
prefs: { storage: false },
|
||||||
|
projectPrefs: false
|
||||||
|
});
|
||||||
|
const target = document.createElement('div');
|
||||||
|
document.body.appendChild(target);
|
||||||
|
const events: string[] = [];
|
||||||
|
const onValueChange = vi.fn((_value: ImageAdjustmentValues) => events.push('value'));
|
||||||
|
const onFilterChange = vi.fn((_filter: string) => events.push('filter'));
|
||||||
|
const component = mount(ImageAdjustmentsAuditHarness, {
|
||||||
|
target,
|
||||||
|
props: { uix, onValueChange, onFilterChange }
|
||||||
|
});
|
||||||
|
flushSync();
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
onValueChange,
|
||||||
|
onFilterChange,
|
||||||
|
events,
|
||||||
|
root: target.querySelector<HTMLElement>('[data-image-adjustments]')!,
|
||||||
|
value(): ImageAdjustmentValues {
|
||||||
|
return JSON.parse(target.querySelector('[data-adjustments-value]')!.textContent!);
|
||||||
|
},
|
||||||
|
button(text: string) {
|
||||||
|
return Array.from(target.querySelectorAll('button')).find(
|
||||||
|
(element) => element.textContent?.trim() === text
|
||||||
|
)!;
|
||||||
|
},
|
||||||
|
async dispose() {
|
||||||
|
await unmount(component);
|
||||||
|
uix.dispose();
|
||||||
|
target.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in new issue