From 2bb8f9c6973a1c4a8ac89cee15cf4979a4113146 Mon Sep 17 00:00:00 2001 From: Stanislav Ulegin Date: Fri, 10 Apr 2026 18:01:53 +0300 Subject: [PATCH] release(content-suggestions): v0.2.1 --- ...content-text-with-suggestions.component.js | 4 +- ...ontent-title-with-suggestions.component.js | 4 +- packages/content-suggestions/package.json | 8 +- ...content-text-with-suggestions.component.ts | 2 +- ...ontent-title-with-suggestions.component.ts | 2 +- .../src/angular/index.test.ts | 97 ++++++++++ .../ContentSuggestions.AngularDOM.stories.tsx | 177 ++++++++++++++++++ 7 files changed, 284 insertions(+), 10 deletions(-) create mode 100644 packages/content-suggestions/src/angular/index.test.ts create mode 100644 packages/content-suggestions/src/stories/ContentSuggestions.AngularDOM.stories.tsx diff --git a/packages/content-suggestions/dist/angular/angular/content-text-with-suggestions.component.js b/packages/content-suggestions/dist/angular/angular/content-text-with-suggestions.component.js index f89735b..72e9ce8 100644 --- a/packages/content-suggestions/dist/angular/angular/content-text-with-suggestions.component.js +++ b/packages/content-suggestions/dist/angular/angular/content-text-with-suggestions.component.js @@ -29,7 +29,7 @@ export class ContentTextWithSuggestionsComponent { [renderLink]="renderLink" (viewed)="viewed.emit()" (expandedChange)="expandedChange.emit($event)" - /> + > `, 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)" - /> + > `, }] }], propDecorators: { className: [{ diff --git a/packages/content-suggestions/dist/angular/angular/content-title-with-suggestions.component.js b/packages/content-suggestions/dist/angular/angular/content-title-with-suggestions.component.js index 45ebca8..e93828a 100644 --- a/packages/content-suggestions/dist/angular/angular/content-title-with-suggestions.component.js +++ b/packages/content-suggestions/dist/angular/angular/content-title-with-suggestions.component.js @@ -34,7 +34,7 @@ export class ContentTitleWithSuggestionsComponent { [renderLink]="renderLink" (viewed)="viewed.emit()" (expandedChange)="expandedChange.emit($event)" - /> + > `, 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)" - /> + > `, }] }], propDecorators: { className: [{ diff --git a/packages/content-suggestions/package.json b/packages/content-suggestions/package.json index f6a2c20..fb44da7 100644 --- a/packages/content-suggestions/package.json +++ b/packages/content-suggestions/package.json @@ -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", diff --git a/packages/content-suggestions/src/angular/content-text-with-suggestions.component.ts b/packages/content-suggestions/src/angular/content-text-with-suggestions.component.ts index 5b0464f..b085c19 100644 --- a/packages/content-suggestions/src/angular/content-text-with-suggestions.component.ts +++ b/packages/content-suggestions/src/angular/content-text-with-suggestions.component.ts @@ -30,7 +30,7 @@ import { ContentTextComponent } from "./content-text.component"; [renderLink]="renderLink" (viewed)="viewed.emit()" (expandedChange)="expandedChange.emit($event)" - /> + > `, }) export class ContentTextWithSuggestionsComponent { diff --git a/packages/content-suggestions/src/angular/content-title-with-suggestions.component.ts b/packages/content-suggestions/src/angular/content-title-with-suggestions.component.ts index 4bf3670..41891a8 100644 --- a/packages/content-suggestions/src/angular/content-title-with-suggestions.component.ts +++ b/packages/content-suggestions/src/angular/content-title-with-suggestions.component.ts @@ -29,7 +29,7 @@ import { ContentTextWithSuggestionsComponent } from "./content-text-with-suggest [renderLink]="renderLink" (viewed)="viewed.emit()" (expandedChange)="expandedChange.emit($event)" - /> + > `, }) export class ContentTitleWithSuggestionsComponent { diff --git a/packages/content-suggestions/src/angular/index.test.ts b/packages/content-suggestions/src/angular/index.test.ts new file mode 100644 index 0000000..29c3fcc --- /dev/null +++ b/packages/content-suggestions/src/angular/index.test.ts @@ -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: ` + + + `, +}) +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"); + }); +}); diff --git a/packages/content-suggestions/src/stories/ContentSuggestions.AngularDOM.stories.tsx b/packages/content-suggestions/src/stories/ContentSuggestions.AngularDOM.stories.tsx new file mode 100644 index 0000000..0e67e4a --- /dev/null +++ b/packages/content-suggestions/src/stories/ContentSuggestions.AngularDOM.stories.tsx @@ -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; + +type MountedTextComponent = ReturnType>; +type MountedTitleComponent = ReturnType>; + +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(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 ( +
+
+ status={status} +
+
+
+ ); +}; + +export const TextAndTitleRenderCheck: Story = { + render: () => , + 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"); + }, +};