feat: add aria label, fix test, transition constant for better testing
This commit is contained in:
@@ -22,9 +22,9 @@
|
||||
<CropInline />
|
||||
|
||||
<div class="crop-controls">
|
||||
<Button label="Загрузить" icon={Upload} type="secondary" />
|
||||
<Button label="Редактировать" icon={Reset} type="outline" />
|
||||
<Button label="Сбросить" icon={Pencil} type="outline" />
|
||||
<Button label="Загрузить" ariaLabel="Upload" icon={Upload} type="secondary" />
|
||||
<Button label="Редактировать" ariaLabel="Edit" icon={Reset} type="outline" />
|
||||
<Button label="Сбросить" ariaLabel="Reset" icon={Pencil} type="outline" />
|
||||
</div>
|
||||
|
||||
<SettingsGrid>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Button from "$components/ui/Button.svelte";
|
||||
import type { SlideDirectionType } from "$lib/constants";
|
||||
import { PANEL_SETTINGS } from "$lib/constants";
|
||||
import ChevronLeft from "~icons/lucide/chevron-left";
|
||||
import ChevronRight from "~icons/lucide/chevron-right";
|
||||
|
||||
@@ -25,13 +24,11 @@
|
||||
if (current < max - 1) current++;
|
||||
direction = "next";
|
||||
}
|
||||
|
||||
let xDirection = $derived(direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH);
|
||||
</script>
|
||||
|
||||
<Button icon={ChevronLeft} type="mini" onclick={prev} disabled={isFirst} />
|
||||
<Button icon={ChevronLeft} ariaLabel="Previous slide" type="mini" onclick={prev} disabled={isFirst} />
|
||||
<span class="panel-indicator">{current + 1} / {max}</span>
|
||||
<Button icon={ChevronRight} type="mini" onclick={next} disabled={isLast} />
|
||||
<Button icon={ChevronRight} ariaLabel="Next slide" type="mini" onclick={next} disabled={isLast} />
|
||||
|
||||
<style>
|
||||
.panel-indicator {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Card from "$components/layout/Card.svelte";
|
||||
import Badge from "$components/ui/Badge.svelte";
|
||||
import Button from "$components/ui/Button.svelte";
|
||||
import { PANEL_SETTINGS, type SlideDirectionType } from "$lib/constants";
|
||||
import { PANEL_SETTINGS, TRANSITION_DURATION, type SlideDirectionType } from "$lib/constants";
|
||||
import { downloadService, type DownloadItem } from "$services/downloadService";
|
||||
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
|
||||
import { konvaStageState } from "$states/konvaStage.svelte";
|
||||
@@ -62,8 +62,8 @@
|
||||
{@const text = textsState?.texts[current]?.text}
|
||||
<div
|
||||
class="konva-wrapper"
|
||||
in:fly={{ x: xDirection, duration: 300 }}
|
||||
out:fly={{ x: -xDirection, duration: 300 }}
|
||||
in:fly={{ x: xDirection, duration: TRANSITION_DURATION }}
|
||||
out:fly={{ x: -xDirection, duration: TRANSITION_DURATION }}
|
||||
>
|
||||
<Preview {text} bind:stage={konvaStageState.stage} />
|
||||
</div>
|
||||
@@ -77,8 +77,8 @@
|
||||
</div>
|
||||
|
||||
<div class="panel-actions">
|
||||
<Button label="Скачать всё" icon={Download} onclick={downloadAll} />
|
||||
<Button label="Скачать" type="outline" icon={Download} onclick={downloadCurrent} />
|
||||
<Button label="Скачать всё" ariaLabel="Download all" icon={Download} onclick={downloadAll} />
|
||||
<Button label="Скачать" ariaLabel="Download current" type="outline" icon={Download} onclick={downloadCurrent} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Button from "$components/ui/Button.svelte";
|
||||
import { TRANSITION_DURATION } from "$lib/constants";
|
||||
import { fly } from "svelte/transition";
|
||||
import Cross from "~icons/lucide/x";
|
||||
|
||||
@@ -12,10 +13,10 @@
|
||||
let { text = $bindable(), id, ondelete }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="text-item" transition:fly={{ x: 600, duration: 300 }}>
|
||||
<li class="text-item" transition:fly={{ x: 600, duration: TRANSITION_DURATION }}>
|
||||
<input type="text" bind:value={text} />
|
||||
<Button icon={Cross} type="danger" onclick={() => ondelete(id)} />
|
||||
</div>
|
||||
<Button icon={Cross} ariaLabel="Delete" type="danger" onclick={() => ondelete(id)} />
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.text-item {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
text: string;
|
||||
ariaLabel: string;
|
||||
onenter: () => void;
|
||||
}
|
||||
|
||||
let { text = $bindable(), onenter }: Props = $props();
|
||||
let { text = $bindable(), onenter, ariaLabel }: Props = $props();
|
||||
|
||||
function handleKeyboard(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
@@ -13,7 +14,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<input bind:value={text} class="text-input" type="text" placeholder="Введите текст..." onkeydown={handleKeyboard} />
|
||||
<input
|
||||
aria-label={ariaLabel}
|
||||
bind:value={text}
|
||||
class="text-input"
|
||||
type="text"
|
||||
placeholder="Введите текст..."
|
||||
onkeydown={handleKeyboard}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.text-input {
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
|
||||
<Card title="Тексты панелей">
|
||||
<InputGroup>
|
||||
<TextInput bind:text onenter={addText} />
|
||||
<Button icon={Plus} onclick={addText} />
|
||||
<TextInput ariaLabel="Input new text" bind:text onenter={addText} />
|
||||
<Button icon={Plus} ariaLabel="Add text" onclick={addText} />
|
||||
</InputGroup>
|
||||
<div class="texts-list">
|
||||
<ul class="texts-list">
|
||||
{#each textsState.texts as { text, id } (id)}
|
||||
<TextInlineEdit {id} {text} ondelete={() => deleteText(id)} />
|
||||
{/each}
|
||||
</div>
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
interface Props {
|
||||
icon: Component;
|
||||
ariaLabel: string;
|
||||
onclick?: MouseEventHandler<HTMLButtonElement>;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
type?: "primary" | "secondary" | "danger" | "outline" | "mini";
|
||||
}
|
||||
|
||||
let { icon: Icon, onclick = () => {}, disabled = false, label = "", type = "primary" }: Props = $props();
|
||||
let { icon: Icon, ariaLabel, onclick = () => {}, disabled = false, label = "", type = "primary" }: Props = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
@@ -22,6 +23,7 @@
|
||||
class:btn-mini={type === "mini"}
|
||||
{disabled}
|
||||
{onclick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<Icon />
|
||||
{label}
|
||||
|
||||
@@ -65,3 +65,5 @@ export const TextAlign = {
|
||||
|
||||
export type TextAlignType = (typeof TextAlign)[keyof typeof TextAlign];
|
||||
export const DEFAULT_TEXT_ALIGN: TextAlignType = TextAlign.CENTER;
|
||||
|
||||
export const TRANSITION_DURATION = import.meta.env.MODE === "test" ? 0 : 300;
|
||||
|
||||
@@ -75,6 +75,11 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:global(ul) {
|
||||
list-style: none;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif;
|
||||
background: var(--bg-primary);
|
||||
|
||||
@@ -8,6 +8,7 @@ describe("TextInput.svelte", () => {
|
||||
props: {
|
||||
text: "Test text",
|
||||
onenter: vi.fn(),
|
||||
ariaLabel: "Test input",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,6 +22,7 @@ describe("TextInput.svelte", () => {
|
||||
props: {
|
||||
text: "Initial",
|
||||
onenter: vi.fn(),
|
||||
ariaLabel: "Test input",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -37,6 +39,7 @@ describe("TextInput.svelte", () => {
|
||||
props: {
|
||||
text: "Test",
|
||||
onenter,
|
||||
ariaLabel: "Test input",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,6 +56,7 @@ describe("TextInput.svelte", () => {
|
||||
props: {
|
||||
text: "Test",
|
||||
onenter,
|
||||
ariaLabel: "Test input",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,194 @@
|
||||
import TextManager from "$components/text/TextManager.svelte";
|
||||
import { textsState } from "$states/texts.svelte";
|
||||
import { render, screen } from "@testing-library/svelte";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/svelte";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("TextManager.svelte", () => {
|
||||
beforeEach(() => {
|
||||
while (textsState.texts.length > 0) {
|
||||
textsState.texts.pop();
|
||||
}
|
||||
textsState.clear();
|
||||
});
|
||||
|
||||
it("should render without crashing", () => {
|
||||
describe("Rendering", () => {
|
||||
it("should render card with title, input and add button", () => {
|
||||
render(TextManager);
|
||||
|
||||
const cardTitle = screen.getByText("Тексты панелей");
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add/i });
|
||||
expect(addButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show empty list when textsState.texts is empty", () => {
|
||||
render(TextManager);
|
||||
|
||||
const textItems = screen.queryAllByRole("listitem");
|
||||
expect(textItems).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe("Adding text", () => {
|
||||
it("should call textsState.addText when add button is clicked", async () => {
|
||||
const addTextSpy = vi.spyOn(textsState, "addText");
|
||||
|
||||
render(TextManager);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
const addButton = screen.getByRole("button", { name: /add/i });
|
||||
|
||||
fireEvent.input(input, { target: { value: "New text" } });
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(addTextSpy).toHaveBeenCalledWith("New text");
|
||||
});
|
||||
|
||||
it("should clear input after adding text", async () => {
|
||||
render(TextManager);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
const addButton = screen.getByRole("button", { name: /add/i });
|
||||
|
||||
fireEvent.input(input, { target: { value: "New text" } });
|
||||
fireEvent.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
||||
it("should add text when Enter key is pressed", () => {
|
||||
const addTextSpy = vi.spyOn(textsState, "addText");
|
||||
|
||||
render(TextManager);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
fireEvent.input(input, { target: { value: "New text" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(addTextSpy).toHaveBeenCalledWith("New text");
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe("List and deletion", () => {
|
||||
it("should render TextInlineEdit for each text", () => {
|
||||
textsState.addText("Text 1");
|
||||
textsState.addText("Text 2");
|
||||
textsState.addText("Text 3");
|
||||
|
||||
render(TextManager);
|
||||
|
||||
const textItems = screen.queryAllByRole("listitem");
|
||||
expect(textItems).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should pass correct props to TextInlineEdit", () => {
|
||||
textsState.addText("Test text");
|
||||
const textId = textsState.texts[0].id;
|
||||
|
||||
render(TextManager);
|
||||
|
||||
const textItems = screen.queryAllByRole("listitem");
|
||||
expect(textItems).toHaveLength(1);
|
||||
|
||||
const inputs = textItems[0].querySelectorAll('input[type="text"]');
|
||||
expect(inputs).toHaveLength(1);
|
||||
expect(inputs[0]).toHaveValue("Test text");
|
||||
});
|
||||
|
||||
it("should call textsState.removeText when delete button is clicked", () => {
|
||||
textsState.addText("Text to delete");
|
||||
const textId = textsState.texts[0].id;
|
||||
const removeTextSpy = vi.spyOn(textsState, "removeText");
|
||||
|
||||
render(TextManager);
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: /delete/i });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(removeTextSpy).toHaveBeenCalledWith(textId);
|
||||
});
|
||||
|
||||
it("should remove element from DOM after deletion", async () => {
|
||||
textsState.addText("Text to delete");
|
||||
|
||||
render(TextManager);
|
||||
|
||||
let textItems = screen.queryAllByRole("listitem");
|
||||
expect(textItems).toHaveLength(1);
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: /delete/i });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
textItems = screen.queryAllByRole("listitem");
|
||||
expect(textItems).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reactivity", () => {
|
||||
it("should update list when textsState.texts changes externally", async () => {
|
||||
render(TextManager);
|
||||
|
||||
let textItems = screen.queryAllByRole("listitem");
|
||||
expect(textItems).toHaveLength(0);
|
||||
|
||||
textsState.addText("New text");
|
||||
|
||||
await waitFor(() => {
|
||||
textItems = screen.queryAllByRole("listitem");
|
||||
expect(textItems).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should update when text is removed externally", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
textsState.addText("Text to remove");
|
||||
const textId = textsState.texts[0].id;
|
||||
|
||||
render(TextManager);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByRole("listitem")).toHaveLength(1);
|
||||
});
|
||||
|
||||
textsState.removeText(textId);
|
||||
await vi.advanceTimersByTimeAsync(400);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
|
||||
});
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
// it("should keep focus in input after adding text", async () => {
|
||||
// render(TextManager);
|
||||
// const input = screen.getByRole("textbox");
|
||||
// const addButton = screen.getByRole("button", { name: /add/i });
|
||||
// fireEvent.input(input, { target: { value: "New text" } });
|
||||
// fireEvent.click(addButton);
|
||||
// await waitFor(() => {
|
||||
// expect(input).toHaveFocus();
|
||||
// });
|
||||
// });
|
||||
|
||||
it("should have proper aria-labels for input and button", () => {
|
||||
render(TextManager);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("aria-label", "Input new text");
|
||||
const addButton = screen.getByRole("button", { name: /add/i });
|
||||
expect(addButton).toHaveAttribute("aria-label", "Add text");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ describe("Button.svelte", () => {
|
||||
props: {
|
||||
icon: MockIcon,
|
||||
label: "Test Button",
|
||||
ariaLabel: "Test button",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,6 +21,7 @@ describe("Button.svelte", () => {
|
||||
const { container } = render(Button, {
|
||||
props: {
|
||||
icon: MockIcon,
|
||||
ariaLabel: "Test button",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,6 +35,7 @@ describe("Button.svelte", () => {
|
||||
icon: MockIcon,
|
||||
label: "Click me",
|
||||
onclick,
|
||||
ariaLabel: "Test button",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,6 +52,7 @@ describe("Button.svelte", () => {
|
||||
icon: MockIcon,
|
||||
label: "Disabled",
|
||||
disabled: true,
|
||||
ariaLabel: "Test button",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,6 +65,7 @@ describe("Button.svelte", () => {
|
||||
props: {
|
||||
icon: MockIcon,
|
||||
label: "Enabled",
|
||||
ariaLabel: "Test button",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user