@kubex/zinc 1.1.118 → 1.1.119
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.
- package/dist/custom-elements.json +1743 -1609
- package/dist/vscode.html-custom-data.json +138 -133
- package/dist/web-types.json +313 -303
- package/dist/zn.d.ts +54 -21
- package/dist/zn.min.js +334 -317
- package/docs/pages/components/form-group.md +2 -0
- package/package.json +1 -1
- package/src/components/form-group/form-group.component.ts +51 -86
- package/src/components/form-group/form-group.test.ts +21 -1
- package/src/components/remarkd-editor/actions.ts +2 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +251 -9
- package/src/components/remarkd-editor/remarkd-editor.scss +49 -0
- package/src/components/remarkd-editor/remarkd-editor.test.ts +329 -5
|
@@ -331,7 +331,7 @@ Second"></zn-remarkd-editor>`);
|
|
|
331
331
|
|
|
332
332
|
it('should group the toolbar buttons', async () => {
|
|
333
333
|
const el = await fixture<ZnRemarkdEditor>(html`
|
|
334
|
-
<zn-remarkd-editor include-url="/includes"></zn-remarkd-editor>`);
|
|
334
|
+
<zn-remarkd-editor include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
|
|
335
335
|
const groups = el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar .toolbar__group');
|
|
336
336
|
|
|
337
337
|
expect(groups.length).to.be.greaterThan(5);
|
|
@@ -367,7 +367,7 @@ Second"></zn-remarkd-editor>`);
|
|
|
367
367
|
const reachableButtons = [...el.shadowRoot!.querySelectorAll<HTMLElement>('.toolbar__group')]
|
|
368
368
|
.filter(group => getComputedStyle(group).display !== 'none')
|
|
369
369
|
.flatMap(group => [...group.querySelectorAll('zn-button')]);
|
|
370
|
-
const expectedCount = EDITOR_ACTIONS.filter(action => action.opens !== 'include').length;
|
|
370
|
+
const expectedCount = EDITOR_ACTIONS.filter(action => action.opens !== 'include' && action.opens !== 'link').length;
|
|
371
371
|
expect(reachableButtons.length + menuItems.length, 'an action fell through the cracks')
|
|
372
372
|
.to.equal(expectedCount);
|
|
373
373
|
});
|
|
@@ -547,10 +547,14 @@ Second"></zn-remarkd-editor>`);
|
|
|
547
547
|
|
|
548
548
|
it('should disable inline actions when no block is being edited', async () => {
|
|
549
549
|
const el = await fixture<ZnRemarkdEditor>(html`
|
|
550
|
-
<zn-remarkd-editor value="hello"></zn-remarkd-editor>`);
|
|
550
|
+
<zn-remarkd-editor value="hello" link-url="/links"></zn-remarkd-editor>`);
|
|
551
551
|
const strong = [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
|
|
552
552
|
.find(b => b.getAttribute('tooltip') === 'Strong')!;
|
|
553
553
|
expect(strong.hasAttribute('disabled')).to.be.true;
|
|
554
|
+
|
|
555
|
+
const link = [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
|
|
556
|
+
.find(b => b.getAttribute('tooltip') === 'Link to article')!;
|
|
557
|
+
expect(link.hasAttribute('disabled')).to.be.true;
|
|
554
558
|
});
|
|
555
559
|
|
|
556
560
|
it('should toggle off an asymmetric mark instead of double-wrapping it', async () => {
|
|
@@ -744,7 +748,7 @@ Second"></zn-remarkd-editor>`);
|
|
|
744
748
|
|
|
745
749
|
it('should list every action in the slash menu under its group', async () => {
|
|
746
750
|
const el = await fixture<ZnRemarkdEditor>(html`
|
|
747
|
-
<zn-remarkd-editor value="Hello" include-url="/includes"></zn-remarkd-editor>`);
|
|
751
|
+
<zn-remarkd-editor value="Hello" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
|
|
748
752
|
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
749
753
|
await el.updateComplete;
|
|
750
754
|
typeInBlock(el, '/');
|
|
@@ -764,10 +768,36 @@ Second"></zn-remarkd-editor>`);
|
|
|
764
768
|
await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
|
|
765
769
|
|
|
766
770
|
const menu = el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu')!;
|
|
767
|
-
expect(menu.items.length).to.equal(EDITOR_ACTIONS.length -
|
|
771
|
+
expect(menu.items.length).to.equal(EDITOR_ACTIONS.length - 2);
|
|
768
772
|
expect(menu.items.every(item => item.action !== 'include')).to.be.true;
|
|
769
773
|
});
|
|
770
774
|
|
|
775
|
+
it('should omit the article link action without a link-url', async () => {
|
|
776
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
777
|
+
<zn-remarkd-editor value="Hello" include-url="/includes"></zn-remarkd-editor>`);
|
|
778
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
779
|
+
await el.updateComplete;
|
|
780
|
+
typeInBlock(el, '/');
|
|
781
|
+
await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
|
|
782
|
+
|
|
783
|
+
const menu = el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu')!;
|
|
784
|
+
expect(menu.items.length).to.equal(EDITOR_ACTIONS.length - 1);
|
|
785
|
+
expect(menu.items.every(item => item.action !== 'link')).to.be.true;
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('should offer the article link action with a link-url', async () => {
|
|
789
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
790
|
+
<zn-remarkd-editor value="Hello" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
|
|
791
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
792
|
+
await el.updateComplete;
|
|
793
|
+
typeInBlock(el, '/');
|
|
794
|
+
await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
|
|
795
|
+
|
|
796
|
+
const menu = el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu')!;
|
|
797
|
+
expect(menu.items.length).to.equal(EDITOR_ACTIONS.length);
|
|
798
|
+
expect(menu.items.some(item => item.action === 'link' && item.label === 'Link to article')).to.be.true;
|
|
799
|
+
});
|
|
800
|
+
|
|
771
801
|
it('should not open the slash menu part-way through a block', async () => {
|
|
772
802
|
const el = await fixture<ZnRemarkdEditor>(html`
|
|
773
803
|
<zn-remarkd-editor value="Hello"></zn-remarkd-editor>`);
|
|
@@ -1343,4 +1373,298 @@ const x = {product};
|
|
|
1343
1373
|
await el.updateComplete;
|
|
1344
1374
|
expect(el.shadowRoot!.querySelectorAll('.remarkd-editor__var').length).to.equal(0);
|
|
1345
1375
|
});
|
|
1376
|
+
|
|
1377
|
+
/** Stubs the link options endpoint, capturing every URL it is called with. */
|
|
1378
|
+
function stubLinkSearch(items: {ref: string; kind: string; title: string; context?: string; status?: string}[]) {
|
|
1379
|
+
const original = window.fetch;
|
|
1380
|
+
const calls: string[] = [];
|
|
1381
|
+
window.fetch = (input: RequestInfo | URL) => {
|
|
1382
|
+
calls.push(String(input));
|
|
1383
|
+
return Promise.resolve(new Response(JSON.stringify({items}),
|
|
1384
|
+
{headers: {'Content-Type': 'application/json'}}));
|
|
1385
|
+
};
|
|
1386
|
+
afterEach(() => {
|
|
1387
|
+
window.fetch = original;
|
|
1388
|
+
});
|
|
1389
|
+
return calls;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
it('should search the link endpoint from the picker', async () => {
|
|
1393
|
+
const calls = stubLinkSearch([
|
|
1394
|
+
{ref: 'kb:document/doc1', kind: 'document', title: 'Refunds', context: 'Billing', status: 'published'},
|
|
1395
|
+
]);
|
|
1396
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1397
|
+
<zn-remarkd-editor value="Hello" link-url="/links"></zn-remarkd-editor>`);
|
|
1398
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
1399
|
+
await el.updateComplete;
|
|
1400
|
+
// "article" matches only this action's label — "/link" would leave the plain
|
|
1401
|
+
// Link action active, since its label sorts into the same band first.
|
|
1402
|
+
const input = typeInBlock(el, '/article');
|
|
1403
|
+
await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
|
|
1404
|
+
input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true, cancelable: true}));
|
|
1405
|
+
|
|
1406
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-picker'),
|
|
1407
|
+
'the link picker never opened');
|
|
1408
|
+
const filter = el.shadowRoot!.querySelector<HTMLInputElement>('.remarkd-editor__link-filter')!;
|
|
1409
|
+
filter.value = 'refund';
|
|
1410
|
+
filter.dispatchEvent(new Event('input', {bubbles: true}));
|
|
1411
|
+
|
|
1412
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-option'),
|
|
1413
|
+
'no results rendered');
|
|
1414
|
+
expect(calls.some(url => url.includes('/links?q=refund'))).to.be.true;
|
|
1415
|
+
const option = el.shadowRoot!.querySelector('.remarkd-editor__link-option')!;
|
|
1416
|
+
expect(option.querySelector('.remarkd-editor__link-option-title')!.textContent).to.contain('Refunds');
|
|
1417
|
+
expect(option.querySelector('.remarkd-editor__link-option-meta')!.textContent).to.contain('Billing');
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
it('should keep the link picker open when opened from the overflow menu', async () => {
|
|
1421
|
+
stubLinkSearch([]);
|
|
1422
|
+
// Narrow enough that every action, including "Link to article", collapses into the
|
|
1423
|
+
// overflow menu — the toolbar-bar buttons guard against blur with @mousedown
|
|
1424
|
+
// preventDefault, but a menu item does not, so this is the path that can race.
|
|
1425
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1426
|
+
<zn-remarkd-editor value="Hello" link-url="/links" style="width: 240px"></zn-remarkd-editor>`);
|
|
1427
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
1428
|
+
await el.updateComplete;
|
|
1429
|
+
|
|
1430
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__toolbar-more'),
|
|
1431
|
+
'the overflow trigger never appeared');
|
|
1432
|
+
const dropdown = el.shadowRoot!.querySelector<ZnDropdown>('.remarkd-editor__toolbar-more')!;
|
|
1433
|
+
await dropdown.show();
|
|
1434
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
1435
|
+
|
|
1436
|
+
const linkItem = [...dropdown.querySelectorAll('zn-menu-item')]
|
|
1437
|
+
.find(item => item.textContent?.includes('Link to article'));
|
|
1438
|
+
expect(linkItem, 'the article-link action should be reachable through the overflow menu').to.exist;
|
|
1439
|
+
// ZnButton overrides click() without dispatching a DOM event, so the menu item's
|
|
1440
|
+
// own click listener (and zn-menu's selection handling) needs a real event.
|
|
1441
|
+
linkItem!.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
|
|
1442
|
+
await el.updateComplete;
|
|
1443
|
+
|
|
1444
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-picker'),
|
|
1445
|
+
'the link picker never opened');
|
|
1446
|
+
// The dropdown's own @zn-hide fires as the menu closes after the item is chosen; it must
|
|
1447
|
+
// not clear suppressBlurCommit out from under the picker it just opened.
|
|
1448
|
+
expect(el.shadowRoot!.querySelector('.remarkd-editor__link-picker'),
|
|
1449
|
+
'the picker unmounted, so the deferred blur committed the edit and closed the block')
|
|
1450
|
+
.to.exist;
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('should apply only the newest search response when an older one resolves later', async () => {
|
|
1454
|
+
const resolvers: Record<string, (response: Response) => void> = {};
|
|
1455
|
+
const original = window.fetch;
|
|
1456
|
+
window.fetch = (input: RequestInfo | URL) => {
|
|
1457
|
+
const q = new URL(String(input), window.location.href).searchParams.get('q') ?? '';
|
|
1458
|
+
return new Promise<Response>(resolve => {
|
|
1459
|
+
resolvers[q] = resolve;
|
|
1460
|
+
});
|
|
1461
|
+
};
|
|
1462
|
+
afterEach(() => {
|
|
1463
|
+
window.fetch = original;
|
|
1464
|
+
});
|
|
1465
|
+
const respond = (items: {ref: string; kind: string; title: string}[]) =>
|
|
1466
|
+
new Response(JSON.stringify({items}), {headers: {'Content-Type': 'application/json'}});
|
|
1467
|
+
|
|
1468
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1469
|
+
<zn-remarkd-editor value="Hello" link-url="/links"></zn-remarkd-editor>`);
|
|
1470
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
1471
|
+
await el.updateComplete;
|
|
1472
|
+
const input = typeInBlock(el, '/article');
|
|
1473
|
+
await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
|
|
1474
|
+
input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true, cancelable: true}));
|
|
1475
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-picker'),
|
|
1476
|
+
'the link picker never opened');
|
|
1477
|
+
|
|
1478
|
+
const filter = el.shadowRoot!.querySelector<HTMLInputElement>('.remarkd-editor__link-filter')!;
|
|
1479
|
+
filter.value = 'first';
|
|
1480
|
+
filter.dispatchEvent(new Event('input', {bubbles: true}));
|
|
1481
|
+
await aTimeout(250);
|
|
1482
|
+
filter.value = 'second';
|
|
1483
|
+
filter.dispatchEvent(new Event('input', {bubbles: true}));
|
|
1484
|
+
await aTimeout(250);
|
|
1485
|
+
|
|
1486
|
+
// Resolve out of order: the stale "first" request settles after the current
|
|
1487
|
+
// "second" one, and must not be allowed to overwrite it.
|
|
1488
|
+
resolvers['second']!(respond([{ref: 'kb:document/second', kind: 'document', title: 'Second Result'}]));
|
|
1489
|
+
await aTimeout(0);
|
|
1490
|
+
resolvers['first']!(respond([{ref: 'kb:document/first', kind: 'document', title: 'First Result'}]));
|
|
1491
|
+
|
|
1492
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-option'),
|
|
1493
|
+
'no results rendered');
|
|
1494
|
+
// An option already exists from the "second" response, so waiting on its mere
|
|
1495
|
+
// presence would race the still-settling "first" response — give it time to
|
|
1496
|
+
// land (and, unguarded, overwrite) before asserting it did not.
|
|
1497
|
+
await aTimeout(50);
|
|
1498
|
+
const titles = [...el.shadowRoot!.querySelectorAll('.remarkd-editor__link-option-title')]
|
|
1499
|
+
.map(title => title.textContent);
|
|
1500
|
+
expect(titles).to.eql(['Second Result']);
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
it('should report a failed link search', async () => {
|
|
1504
|
+
const original = window.fetch;
|
|
1505
|
+
window.fetch = () => Promise.resolve(new Response('nope', {status: 500}));
|
|
1506
|
+
afterEach(() => {
|
|
1507
|
+
window.fetch = original;
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1511
|
+
<zn-remarkd-editor value="Hello" link-url="/links"></zn-remarkd-editor>`);
|
|
1512
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
1513
|
+
await el.updateComplete;
|
|
1514
|
+
const input = typeInBlock(el, '/article');
|
|
1515
|
+
await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
|
|
1516
|
+
input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true, cancelable: true}));
|
|
1517
|
+
|
|
1518
|
+
// The debounced request is still pending when the empty state first renders
|
|
1519
|
+
// ("Searching…"), so wait for the failed message itself, not just the div.
|
|
1520
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-picker-empty')
|
|
1521
|
+
?.textContent?.includes('Could not search'), 'the picker never settled');
|
|
1522
|
+
expect(el.shadowRoot!.querySelector('.remarkd-editor__link-picker-empty')!.textContent)
|
|
1523
|
+
.to.contain('Could not search');
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it('should close the link picker without touching the block', async () => {
|
|
1527
|
+
stubLinkSearch([]);
|
|
1528
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1529
|
+
<zn-remarkd-editor value="Hello" link-url="/links"></zn-remarkd-editor>`);
|
|
1530
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
1531
|
+
await el.updateComplete;
|
|
1532
|
+
const input = typeInBlock(el, '/article');
|
|
1533
|
+
await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
|
|
1534
|
+
input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true, cancelable: true}));
|
|
1535
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-picker'),
|
|
1536
|
+
'the link picker never opened');
|
|
1537
|
+
|
|
1538
|
+
// ZnButton overrides .click() to skip dispatching a real click event, so the
|
|
1539
|
+
// host's own @click listener needs one dispatched directly, as elsewhere in this file.
|
|
1540
|
+
el.shadowRoot!.querySelector<ZnButton>('.remarkd-editor__link-picker zn-button')!
|
|
1541
|
+
.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
|
|
1542
|
+
await el.updateComplete;
|
|
1543
|
+
expect(el.shadowRoot!.querySelector('.remarkd-editor__link-picker')).to.not.exist;
|
|
1544
|
+
expect(el.value).to.equal('Hello');
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Opens the picker from the toolbar over the open block, with the given
|
|
1549
|
+
* selection — the slash path never has one, so the toolbar is the only way to
|
|
1550
|
+
* exercise a selected label. The fixtures below are 4000px wide so no toolbar
|
|
1551
|
+
* group has collapsed into the overflow menu.
|
|
1552
|
+
*/
|
|
1553
|
+
async function openLinkPicker(el: ZnRemarkdEditor, text: string, from: number, to: number) {
|
|
1554
|
+
await aTimeout(50);
|
|
1555
|
+
el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
|
|
1556
|
+
await el.updateComplete;
|
|
1557
|
+
const input = typeInBlock(el, text);
|
|
1558
|
+
input.setSelectionRange(from, to);
|
|
1559
|
+
el.shadowRoot!.querySelector<ZnButton>('zn-button[tooltip="Link to article"]')!
|
|
1560
|
+
.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
|
|
1561
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-picker'),
|
|
1562
|
+
'the link picker never opened');
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
it('should wrap the selected text as the link label', async () => {
|
|
1566
|
+
stubLinkSearch([{ref: 'kb:document/doc1', kind: 'document', title: 'Refunds'}]);
|
|
1567
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1568
|
+
<zn-remarkd-editor style="width: 4000px" value="Read Refunds now" link-url="/links"></zn-remarkd-editor>`);
|
|
1569
|
+
await openLinkPicker(el, 'Read Refunds now', 5, 12);
|
|
1570
|
+
|
|
1571
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-option'), 'no results');
|
|
1572
|
+
el.shadowRoot!.querySelector<HTMLButtonElement>('.remarkd-editor__link-option')!.click();
|
|
1573
|
+
await el.updateComplete;
|
|
1574
|
+
|
|
1575
|
+
const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
|
|
1576
|
+
expect(input.value).to.equal('Read [Refunds](kb:document/doc1) now');
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
it('should insert the title as the label with no selection', async () => {
|
|
1580
|
+
stubLinkSearch([{ref: 'kb:document/doc1', kind: 'document', title: 'Refunds [and] more'}]);
|
|
1581
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1582
|
+
<zn-remarkd-editor style="width: 4000px" value="See " link-url="/links"></zn-remarkd-editor>`);
|
|
1583
|
+
await openLinkPicker(el, 'See ', 4, 4);
|
|
1584
|
+
|
|
1585
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-option'), 'no results');
|
|
1586
|
+
el.shadowRoot!.querySelector<HTMLButtonElement>('.remarkd-editor__link-option')!.click();
|
|
1587
|
+
await el.updateComplete;
|
|
1588
|
+
|
|
1589
|
+
const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
|
|
1590
|
+
expect(input.value).to.equal('See [Refunds (and) more](kb:document/doc1)');
|
|
1591
|
+
expect(el.shadowRoot!.querySelector('.remarkd-editor__link-picker')).to.not.exist;
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
it('should collapse each CRLF character in the title to a space, matching Go byte-for-byte', async () => {
|
|
1595
|
+
// Label formatting must match app-kb's model.ContentLinkMarkup character for character.
|
|
1596
|
+
stubLinkSearch([{ref: 'kb:document/doc1', kind: 'document', title: 'Contact\r\nnow'}]);
|
|
1597
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1598
|
+
<zn-remarkd-editor style="width: 4000px" value="See " link-url="/links"></zn-remarkd-editor>`);
|
|
1599
|
+
await openLinkPicker(el, 'See ', 4, 4);
|
|
1600
|
+
|
|
1601
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-option'), 'no results');
|
|
1602
|
+
el.shadowRoot!.querySelector<HTMLButtonElement>('.remarkd-editor__link-option')!.click();
|
|
1603
|
+
await el.updateComplete;
|
|
1604
|
+
|
|
1605
|
+
const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
|
|
1606
|
+
expect(input.value).to.equal('See [Contact now](kb:document/doc1)');
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
it('should commit the inserted link to the value', async () => {
|
|
1610
|
+
stubLinkSearch([{ref: 'kb:category/cat1', kind: 'category', title: 'Billing'}]);
|
|
1611
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1612
|
+
<zn-remarkd-editor style="width: 4000px" value="See " link-url="/links"></zn-remarkd-editor>`);
|
|
1613
|
+
await openLinkPicker(el, 'See ', 4, 4);
|
|
1614
|
+
|
|
1615
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-option'), 'no results');
|
|
1616
|
+
el.shadowRoot!.querySelector<HTMLButtonElement>('.remarkd-editor__link-option')!.click();
|
|
1617
|
+
await el.updateComplete;
|
|
1618
|
+
el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!
|
|
1619
|
+
.dispatchEvent(new Event('blur', {bubbles: true}));
|
|
1620
|
+
await el.updateComplete;
|
|
1621
|
+
|
|
1622
|
+
expect(el.value).to.equal('See [Billing](kb:category/cat1)');
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
it('should flag a link whose target the app does not know', async () => {
|
|
1626
|
+
const calls = stubLinkSearch([{ref: 'kb:document/doc1', kind: 'document', title: 'Refunds'}]);
|
|
1627
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1628
|
+
<zn-remarkd-editor link-url="/links"
|
|
1629
|
+
value="See [Refunds](kb:document/doc1) and [Gone](kb:document/doc9)."></zn-remarkd-editor>`);
|
|
1630
|
+
|
|
1631
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link--missing'),
|
|
1632
|
+
'the missing state never rendered');
|
|
1633
|
+
expect(calls.some(url => url.includes('refs=') && url.includes('doc9'))).to.be.true;
|
|
1634
|
+
|
|
1635
|
+
const anchors = [...el.shadowRoot!.querySelectorAll<HTMLAnchorElement>('a[href^="kb:"]')];
|
|
1636
|
+
const healthy = anchors.find(a => a.getAttribute('href') === 'kb:document/doc1')!;
|
|
1637
|
+
const missing = anchors.find(a => a.getAttribute('href') === 'kb:document/doc9')!;
|
|
1638
|
+
expect(healthy.classList.contains('remarkd-editor__link--missing')).to.be.false;
|
|
1639
|
+
expect(missing.classList.contains('remarkd-editor__link--missing')).to.be.true;
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
// A list that never arrived is not evidence a link is broken.
|
|
1643
|
+
it('should not flag links when the resolve request fails', async () => {
|
|
1644
|
+
const original = window.fetch;
|
|
1645
|
+
const calls: string[] = [];
|
|
1646
|
+
window.fetch = (input: RequestInfo | URL) => {
|
|
1647
|
+
calls.push(String(input));
|
|
1648
|
+
return Promise.resolve(new Response('nope', {status: 500}));
|
|
1649
|
+
};
|
|
1650
|
+
afterEach(() => {
|
|
1651
|
+
window.fetch = original;
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1655
|
+
<zn-remarkd-editor link-url="/links"
|
|
1656
|
+
value="See [Gone](kb:document/doc9)."></zn-remarkd-editor>`);
|
|
1657
|
+
await aTimeout(200);
|
|
1658
|
+
expect(calls.some(url => url.includes('refs=') && url.includes('doc9'))).to.be.true;
|
|
1659
|
+
expect(el.shadowRoot!.querySelector('.remarkd-editor__link--missing')).to.not.exist;
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
it('should not flag links without a link-url', async () => {
|
|
1663
|
+
const calls = stubLinkSearch([]);
|
|
1664
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
1665
|
+
<zn-remarkd-editor value="See [Gone](kb:document/doc9)."></zn-remarkd-editor>`);
|
|
1666
|
+
await aTimeout(50);
|
|
1667
|
+
expect(calls).to.have.length(0);
|
|
1668
|
+
expect(el.shadowRoot!.querySelector('.remarkd-editor__link--missing')).to.not.exist;
|
|
1669
|
+
});
|
|
1346
1670
|
});
|