Compare commits
1 Commits
tach-typog
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bb8f9c697 |
@@ -29,7 +29,7 @@ export class ContentTextWithSuggestionsComponent {
|
||||
[renderLink]="renderLink"
|
||||
(viewed)="viewed.emit()"
|
||||
(expandedChange)="expandedChange.emit($event)"
|
||||
/>
|
||||
></content-text>
|
||||
`, isInline: true, dependencies: [{ kind: "component", type: ContentTextComponent, selector: "content-text", inputs: ["className", "weight", "text", "ellipsis", "blur", "style", "onView", "renderMention", "renderTag", "renderLink"], outputs: ["viewed", "expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTextWithSuggestionsComponent, decorators: [{
|
||||
@@ -53,7 +53,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
||||
[renderLink]="renderLink"
|
||||
(viewed)="viewed.emit()"
|
||||
(expandedChange)="expandedChange.emit($event)"
|
||||
/>
|
||||
></content-text>
|
||||
`,
|
||||
}]
|
||||
}], propDecorators: { className: [{
|
||||
|
||||
@@ -34,7 +34,7 @@ export class ContentTitleWithSuggestionsComponent {
|
||||
[renderLink]="renderLink"
|
||||
(viewed)="viewed.emit()"
|
||||
(expandedChange)="expandedChange.emit($event)"
|
||||
/>
|
||||
></content-text-with-suggestions>
|
||||
`, isInline: true, dependencies: [{ kind: "component", type: ContentTextWithSuggestionsComponent, selector: "content-text-with-suggestions", inputs: ["className", "weight", "text", "ellipsis", "blur", "style", "onView", "renderMention", "renderTag", "renderLink"], outputs: ["viewed", "expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTitleWithSuggestionsComponent, decorators: [{
|
||||
@@ -58,7 +58,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
||||
[renderLink]="renderLink"
|
||||
(viewed)="viewed.emit()"
|
||||
(expandedChange)="expandedChange.emit($event)"
|
||||
/>
|
||||
></content-text-with-suggestions>
|
||||
`,
|
||||
}]
|
||||
}], propDecorators: { className: [{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hublib-web/content-suggestions",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "Content text/title with mentions, tags and links for React and Angular",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -51,7 +51,7 @@
|
||||
"build:angular": "ngc -p tsconfig.angular.json && node ./scripts/fix-angular-entry.mjs",
|
||||
"clean": "rm -rf dist storybook-static",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test": "yarn run -T vitest run --passWithNoTests",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build"
|
||||
@@ -59,7 +59,7 @@
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=17.0.0",
|
||||
"@angular/core": ">=17.0.0",
|
||||
"@hublib-web/tach-typography": "0.3.0",
|
||||
"@hublib-web/tach-typography": "0.3.1",
|
||||
"antd": ">=5.0.0",
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0",
|
||||
@@ -90,7 +90,7 @@
|
||||
"@angular/compiler": "^20.3.17",
|
||||
"@angular/compiler-cli": "^20.3.17",
|
||||
"@angular/core": "^20.3.17",
|
||||
"@hublib-web/tach-typography": "workspace:0.3.0",
|
||||
"@hublib-web/tach-typography": "workspace:0.3.1",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/react": "8.6.14",
|
||||
"@storybook/react-vite": "8.6.14",
|
||||
|
||||
@@ -30,7 +30,7 @@ import { ContentTextComponent } from "./content-text.component";
|
||||
[renderLink]="renderLink"
|
||||
(viewed)="viewed.emit()"
|
||||
(expandedChange)="expandedChange.emit($event)"
|
||||
/>
|
||||
></content-text>
|
||||
`,
|
||||
})
|
||||
export class ContentTextWithSuggestionsComponent {
|
||||
|
||||
@@ -29,7 +29,7 @@ import { ContentTextWithSuggestionsComponent } from "./content-text-with-suggest
|
||||
[renderLink]="renderLink"
|
||||
(viewed)="viewed.emit()"
|
||||
(expandedChange)="expandedChange.emit($event)"
|
||||
/>
|
||||
></content-text-with-suggestions>
|
||||
`,
|
||||
})
|
||||
export class ContentTitleWithSuggestionsComponent {
|
||||
|
||||
97
packages/content-suggestions/src/angular/index.test.ts
Normal file
97
packages/content-suggestions/src/angular/index.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import "@angular/compiler";
|
||||
|
||||
import { Component, provideZonelessChangeDetection } from "@angular/core";
|
||||
import { TestBed, getTestBed } from "@angular/core/testing";
|
||||
import { BrowserTestingModule, platformBrowserTesting } from "@angular/platform-browser/testing";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
ContentTextWithSuggestionsComponent,
|
||||
ContentTitleWithSuggestionsComponent,
|
||||
} from "../../dist/angular/index.js";
|
||||
|
||||
const ensureAngularTestEnvironment = (): void => {
|
||||
try {
|
||||
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("Cannot set base providers")) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ensureAngularTestEnvironment();
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ContentTextWithSuggestionsComponent, ContentTitleWithSuggestionsComponent],
|
||||
template: `
|
||||
<content-text-with-suggestions [text]="text"></content-text-with-suggestions>
|
||||
<content-title-with-suggestions [text]="title"></content-title-with-suggestions>
|
||||
`,
|
||||
})
|
||||
class ContentSuggestionsHostComponent {
|
||||
text =
|
||||
"Привет @[John Doe](d290f1ee-6c54-4b01-90e6-d701748f0851) #frontend смотри example.com";
|
||||
title = "Заголовок @[Jane Roe](123e4567-e89b-12d3-a456-426614174000) #news docs.example.com";
|
||||
}
|
||||
|
||||
describe("content-suggestions (angular)", () => {
|
||||
afterEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it("renders text with mention, tag and link tokens", async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(ContentSuggestionsHostComponent);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const host: HTMLElement = fixture.nativeElement;
|
||||
const content = host.querySelector("content-text-with-suggestions");
|
||||
expect(content).not.toBeNull();
|
||||
|
||||
const text = content?.textContent ?? "";
|
||||
expect(text).toContain("Привет");
|
||||
expect(text).toContain("@John Doe");
|
||||
expect(text).toContain("#frontend");
|
||||
expect(text).toContain("example.com");
|
||||
|
||||
const link = content?.querySelector('a.tach-typography[href="https://example.com"]');
|
||||
expect(link).not.toBeNull();
|
||||
expect(link?.textContent).toContain("example.com");
|
||||
});
|
||||
|
||||
it("renders title variant and keeps entities visible", async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(ContentSuggestionsHostComponent);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const host: HTMLElement = fixture.nativeElement;
|
||||
const title = host.querySelector("content-title-with-suggestions");
|
||||
expect(title).not.toBeNull();
|
||||
|
||||
const text = title?.textContent ?? "";
|
||||
expect(text).toContain("Заголовок");
|
||||
expect(text).toContain("@Jane Roe");
|
||||
expect(text).toContain("#news");
|
||||
expect(text).toContain("docs.example.com");
|
||||
|
||||
const boldNode = title?.querySelector(".tach-typography--bold");
|
||||
expect(boldNode).not.toBeNull();
|
||||
|
||||
const link = title?.querySelector('a.tach-typography[href="https://docs.example.com"]');
|
||||
expect(link).not.toBeNull();
|
||||
expect(link?.textContent).toContain("docs.example.com");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import "@angular/compiler";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { createComponent, provideZonelessChangeDetection, type ApplicationRef } from "@angular/core";
|
||||
import { createApplication } from "@angular/platform-browser";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { expect, waitFor, within } from "@storybook/test";
|
||||
|
||||
import {
|
||||
ContentTextWithSuggestionsComponent,
|
||||
ContentTitleWithSuggestionsComponent,
|
||||
} from "../../dist/angular/index.js";
|
||||
|
||||
const meta = {
|
||||
title: "Angular/ContentSuggestions DOM",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Angular runtime verification in Storybook: text and title components with mention/tag/link rendered into real DOM.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
type MountedTextComponent = ReturnType<typeof createComponent<ContentTextWithSuggestionsComponent>>;
|
||||
type MountedTitleComponent = ReturnType<typeof createComponent<ContentTitleWithSuggestionsComponent>>;
|
||||
|
||||
const TEXT_SAMPLE =
|
||||
"Angular text: @[Иван](123e4567-e89b-12d3-a456-426614174000) #frontend docs.example.com";
|
||||
const TITLE_SAMPLE =
|
||||
"Angular title: @[Мария](123e4567-e89b-12d3-a456-426614174001) #release github.com";
|
||||
|
||||
const createSection = (title: string): HTMLDivElement => {
|
||||
const section = document.createElement("div");
|
||||
section.style.display = "grid";
|
||||
section.style.gap = "8px";
|
||||
section.style.padding = "10px 12px";
|
||||
section.style.border = "1px solid #e2e8f0";
|
||||
section.style.borderRadius = "10px";
|
||||
|
||||
const heading = document.createElement("div");
|
||||
heading.style.fontFamily =
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Courier New", monospace';
|
||||
heading.style.fontSize = "12px";
|
||||
heading.style.color = "#475569";
|
||||
heading.textContent = title;
|
||||
|
||||
const body = document.createElement("div");
|
||||
section.append(heading, body);
|
||||
body.dataset.slot = "body";
|
||||
|
||||
return section;
|
||||
};
|
||||
|
||||
const AngularContentDomHarness: React.FC = () => {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||
const [status, setStatus] = useState("mounting");
|
||||
|
||||
useEffect(() => {
|
||||
const host = hostRef.current;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
let appRef: ApplicationRef | null = null;
|
||||
let textRef: MountedTextComponent | null = null;
|
||||
let titleRef: MountedTitleComponent | null = null;
|
||||
|
||||
host.innerHTML = "";
|
||||
setStatus("mounting");
|
||||
|
||||
void createApplication({
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
})
|
||||
.then(app => {
|
||||
if (cancelled) {
|
||||
app.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
appRef = app;
|
||||
|
||||
const textSection = createSection("ContentTextWithSuggestionsComponent");
|
||||
const titleSection = createSection("ContentTitleWithSuggestionsComponent");
|
||||
textSection.dataset.testid = "angular-content-text";
|
||||
titleSection.dataset.testid = "angular-content-title";
|
||||
host.append(textSection, titleSection);
|
||||
|
||||
const textHost = textSection.querySelector('[data-slot="body"]') as HTMLElement;
|
||||
const titleHost = titleSection.querySelector('[data-slot="body"]') as HTMLElement;
|
||||
|
||||
textRef = createComponent(ContentTextWithSuggestionsComponent, {
|
||||
environmentInjector: app.injector,
|
||||
hostElement: textHost,
|
||||
});
|
||||
app.attachView(textRef.hostView);
|
||||
textRef.setInput("text", TEXT_SAMPLE);
|
||||
textRef.setInput("ellipsis", false);
|
||||
textRef.changeDetectorRef.detectChanges();
|
||||
|
||||
titleRef = createComponent(ContentTitleWithSuggestionsComponent, {
|
||||
environmentInjector: app.injector,
|
||||
hostElement: titleHost,
|
||||
});
|
||||
app.attachView(titleRef.hostView);
|
||||
titleRef.setInput("text", TITLE_SAMPLE);
|
||||
titleRef.setInput("ellipsis", false);
|
||||
titleRef.changeDetectorRef.detectChanges();
|
||||
|
||||
setStatus("ready");
|
||||
})
|
||||
.catch(error => {
|
||||
setStatus(`error: ${String(error)}`);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
textRef?.destroy();
|
||||
titleRef?.destroy();
|
||||
appRef?.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ maxWidth: 960, width: "100%", display: "grid", gap: 12 }}
|
||||
data-testid="angular-content-dom-story"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
||||
fontSize: 12,
|
||||
color: "#475569",
|
||||
}}
|
||||
>
|
||||
status={status}
|
||||
</div>
|
||||
<div ref={hostRef} style={{ display: "grid", gap: 12 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TextAndTitleRenderCheck: Story = {
|
||||
render: () => <AngularContentDomHarness />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByText("status=ready")).toBeTruthy();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByText("Angular text:")).toBeTruthy();
|
||||
expect(canvas.getByText("@Иван")).toBeTruthy();
|
||||
expect(canvas.getByText("#frontend")).toBeTruthy();
|
||||
expect(canvas.getByText("docs.example.com")).toBeTruthy();
|
||||
expect(canvas.getByText("Angular title:")).toBeTruthy();
|
||||
expect(canvas.getByText("@Мария")).toBeTruthy();
|
||||
expect(canvas.getByText("#release")).toBeTruthy();
|
||||
expect(canvas.getByText("github.com")).toBeTruthy();
|
||||
});
|
||||
|
||||
const docsLink = canvas.getByRole("link", { name: "docs.example.com" });
|
||||
expect(docsLink.getAttribute("href")).toBe("https://docs.example.com");
|
||||
|
||||
const githubLink = canvas.getByRole("link", { name: "github.com" });
|
||||
expect(githubLink.getAttribute("href")).toBe("https://github.com");
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user