This commit is contained in:
@@ -1,55 +1,102 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ContextualChatEntry } from '$lib/game-chat.js';
|
import type { ContextualChatEntry } from '$lib/game-chat.js';
|
||||||
|
import { timestampISO } from '$lib/time-zone.js';
|
||||||
|
import { serverIdentity } from '$ui/server-labels.js';
|
||||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||||
import Badge from '../primitives/Badge.svelte';
|
import Badge from '../primitives/Badge.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
entries,
|
entries,
|
||||||
selectedMessageIds = [],
|
selectedMessageIds = [],
|
||||||
highlightedMessageIds = []
|
highlightedMessageIds = [],
|
||||||
|
matchId,
|
||||||
|
server,
|
||||||
|
startedAt
|
||||||
}: {
|
}: {
|
||||||
entries: ContextualChatEntry[];
|
entries: ContextualChatEntry[];
|
||||||
selectedMessageIds?: string[];
|
selectedMessageIds?: string[];
|
||||||
highlightedMessageIds?: string[];
|
highlightedMessageIds?: string[];
|
||||||
|
matchId?: number | null;
|
||||||
|
server?: string | null;
|
||||||
|
startedAt?: string | null;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
const messages = $derived(entries.filter((entry) => entry.type === 'message'));
|
||||||
|
const context = $derived.by(() => {
|
||||||
|
const times = messages
|
||||||
|
.map((entry) => timestampISO(entry.context.realTime))
|
||||||
|
.filter((time): time is string => !!time)
|
||||||
|
.sort();
|
||||||
|
const dates = [...new Set(times.map((time) => $timeFormat.date(time)))];
|
||||||
|
const rounds = messages
|
||||||
|
.map((entry) => entry.context.round)
|
||||||
|
.filter((round): round is number => Number.isInteger(round));
|
||||||
|
const firstRound = Math.min(...rounds);
|
||||||
|
const lastRound = Math.max(...rounds);
|
||||||
|
return [
|
||||||
|
...(server ? [serverIdentity(server).map] : []),
|
||||||
|
...(matchId ? [`Match #${matchId}`] : []),
|
||||||
|
dates.length
|
||||||
|
? dates.length === 1
|
||||||
|
? dates[0]
|
||||||
|
: `${dates[0]} – ${dates[dates.length - 1]}`
|
||||||
|
: timestampISO(startedAt)
|
||||||
|
? `Match started ${$timeFormat.date(startedAt)}`
|
||||||
|
: 'Date not recorded',
|
||||||
|
rounds.length
|
||||||
|
? `${firstRound === lastRound ? `Round ${firstRound}` : `Rounds ${firstRound}–${lastRound}`}${rounds.length < messages.length ? ' (some unknown)' : ''}`
|
||||||
|
: 'Round not recorded',
|
||||||
|
'Times are estimates.'
|
||||||
|
].join(' · ');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class="excerpt-note">
|
<div class="excerpt-context">
|
||||||
Selected messages and surrounding context in archive order. Messages between separate selections
|
<p>{context}</p>
|
||||||
may be omitted.
|
<p>
|
||||||
</p>
|
Highlighted lines are subject messages. Messages between separate selections may be omitted.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<ol class="excerpt" aria-label="Preserved conversation excerpt">
|
<ol class="excerpt" aria-label="Preserved conversation excerpt">
|
||||||
{#each entries as entry}
|
{#each messages as entry}
|
||||||
{#if entry.type === 'message'}
|
{@const highlighted = !!entry.messageId && highlightedMessageIds.includes(entry.messageId)}
|
||||||
{@const highlighted =
|
{@const selected = !!entry.messageId && selectedMessageIds.includes(entry.messageId)}
|
||||||
!!entry.messageId && highlightedMessageIds.includes(entry.messageId)}
|
{@const realTime = timestampISO(entry.context.realTime)}
|
||||||
{@const selected = !!entry.messageId && selectedMessageIds.includes(entry.messageId)}
|
<li class:highlighted>
|
||||||
<li class:highlighted>
|
<span class="message-time">
|
||||||
<div class="author">
|
{#if realTime}<time datetime={realTime} title={$timeFormat.dateTime(realTime)}
|
||||||
<strong>{entry.message.player}</strong>
|
>{$timeFormat.clockTime(realTime)}</time
|
||||||
{#if entry.message.channel === 'team'}<Badge>Team chat</Badge>{/if}
|
>{:else}<span title="Message time unknown">Unknown time</span>{/if}
|
||||||
{#if highlighted}<Badge variant="warning">Subject message</Badge>
|
</span>
|
||||||
{:else if selected}<Badge>Selected context</Badge>
|
<strong class="author">{entry.message.player}</strong>
|
||||||
{:else}<span>Surrounding context</span>{/if}
|
<p class="message">
|
||||||
</div>
|
<span class="sr-only"
|
||||||
<div class="metadata">
|
>{highlighted
|
||||||
<span>GUID {entry.message.guid}</span>
|
? 'Subject message: '
|
||||||
{#if entry.context.realTime}<span
|
: selected
|
||||||
>Estimated time <time datetime={entry.context.realTime}
|
? 'Selected context: '
|
||||||
>{$timeFormat.dateTime(entry.context.realTime)}</time
|
: 'Surrounding context: '}</span
|
||||||
></span
|
>{#if entry.message.channel === 'team'}<Badge>Team chat</Badge>{' '}{/if}{entry
|
||||||
>{:else}<span>Message time unknown</span>{/if}
|
.message.message}
|
||||||
<span>Log {entry.message.time}</span>
|
</p>
|
||||||
{#if entry.context.round !== null}<span>Round {entry.context.round}</span>{/if}
|
</li>
|
||||||
</div>
|
|
||||||
<p>{entry.message.message}</p>
|
|
||||||
{#if entry.messageId}<details>
|
|
||||||
<summary>Message identifier</summary><code>{entry.messageId}</code>
|
|
||||||
</details>{/if}
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
</ol>
|
</ol>
|
||||||
|
<details class="message-details">
|
||||||
|
<summary>Message details</summary>
|
||||||
|
<ol aria-label="Preserved message details">
|
||||||
|
{#each messages as entry}
|
||||||
|
<li>
|
||||||
|
<strong>{entry.message.player}</strong>
|
||||||
|
<span>GUID <code>{entry.message.guid}</code></span>
|
||||||
|
<span>Original log time · {entry.message.time}</span>
|
||||||
|
{#if Number.isInteger(entry.context.round)}<span>Round {entry.context.round}</span
|
||||||
|
>{/if}
|
||||||
|
{#if entry.messageId}<span>Message identifier <code>{entry.messageId}</code></span
|
||||||
|
>{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.excerpt {
|
.excerpt {
|
||||||
@@ -59,50 +106,63 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
li {
|
.excerpt > li {
|
||||||
padding: var(--space-3);
|
display: grid;
|
||||||
border-left: 3px solid var(--border-subtle);
|
grid-template-columns: 7.5em minmax(6em, 1fr) minmax(0, 3fr);
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-left: 3px solid transparent;
|
||||||
background: var(--surface-sunken);
|
background: var(--surface-sunken);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--leading-body);
|
||||||
}
|
}
|
||||||
li.highlighted {
|
.excerpt > li.highlighted {
|
||||||
border-left-color: var(--state-warning);
|
border-left-color: var(--state-warning);
|
||||||
background: var(--surface-raised);
|
background: var(--surface-raised);
|
||||||
}
|
}
|
||||||
.author,
|
.author,
|
||||||
.metadata {
|
.message {
|
||||||
display: flex;
|
min-width: 0;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
.author > span,
|
.message {
|
||||||
.metadata {
|
white-space: pre-wrap;
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
}
|
}
|
||||||
.metadata {
|
.message-time {
|
||||||
margin-top: var(--space-1);
|
color: var(--text-secondary);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
white-space: pre-wrap;
|
margin: 0;
|
||||||
overflow-wrap: anywhere;
|
|
||||||
margin: var(--space-2) 0 0;
|
|
||||||
line-height: var(--leading-body);
|
|
||||||
}
|
}
|
||||||
details {
|
.excerpt-context,
|
||||||
margin-top: var(--space-2);
|
.message-details {
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-body);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
.message-details ol {
|
||||||
|
padding-left: var(--space-5);
|
||||||
|
}
|
||||||
|
.message-details li {
|
||||||
|
padding: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
.message-details li > span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
summary {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.excerpt-note {
|
@media (max-width: 640px) {
|
||||||
margin: 0;
|
.excerpt > li {
|
||||||
font-size: var(--text-xs);
|
grid-template-columns: 7.5em minmax(0, 1fr);
|
||||||
color: var(--text-secondary);
|
gap: var(--space-1) var(--space-3);
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -250,7 +250,12 @@
|
|||||||
{excerpt.length} messages in this excerpt. Selected subject messages are highlighted.
|
{excerpt.length} messages in this excerpt. Selected subject messages are highlighted.
|
||||||
Messages already preserved as evidence in this case are not attached again.
|
Messages already preserved as evidence in this case are not attached again.
|
||||||
</p>
|
</p>
|
||||||
<EvidenceMessages entries={excerpt} {selectedMessageIds} {highlightedMessageIds} />
|
<EvidenceMessages
|
||||||
|
entries={excerpt}
|
||||||
|
{matchId}
|
||||||
|
{selectedMessageIds}
|
||||||
|
{highlightedMessageIds}
|
||||||
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
|
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
|
||||||
{#if uncertain}<p class="hint">
|
{#if uncertain}<p class="hint">
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export function preservedConversation(value: unknown): {
|
|||||||
} | null {
|
} | null {
|
||||||
if (!value || typeof value !== 'object') return null;
|
if (!value || typeof value !== 'object') return null;
|
||||||
const snapshot = value as Record<string, unknown>;
|
const snapshot = value as Record<string, unknown>;
|
||||||
if (!Array.isArray(snapshot.entries)) return null;
|
const candidates = Array.isArray(snapshot.entries) ? snapshot.entries : [snapshot.entry];
|
||||||
const entries = snapshot.entries.filter(
|
const entries = candidates.filter(
|
||||||
(entry): entry is Extract<ContextualChatEntry, { type: 'message' }> => {
|
(entry): entry is Extract<ContextualChatEntry, { type: 'message' }> => {
|
||||||
if (!entry || typeof entry !== 'object' || entry.type !== 'message') return false;
|
if (!entry || typeof entry !== 'object' || entry.type !== 'message') return false;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -189,17 +189,6 @@
|
|||||||
typeof value.entry?.message?.player === 'string'
|
typeof value.entry?.message?.player === 'string'
|
||||||
? (value.entry.message.player as string)
|
? (value.entry.message.player as string)
|
||||||
: null,
|
: null,
|
||||||
time:
|
|
||||||
typeof value.entry?.message?.time === 'string'
|
|
||||||
? (value.entry.message.time as string)
|
|
||||||
: null,
|
|
||||||
realTime:
|
|
||||||
typeof value.entry?.context?.realTime === 'string'
|
|
||||||
? (value.entry.context.realTime as string)
|
|
||||||
: null,
|
|
||||||
round: Number.isInteger(value.entry?.context?.round)
|
|
||||||
? (value.entry.context.round as number)
|
|
||||||
: null,
|
|
||||||
original: JSON.stringify(value, null, 2),
|
original: JSON.stringify(value, null, 2),
|
||||||
excerpt: preservedConversation(value)
|
excerpt: preservedConversation(value)
|
||||||
};
|
};
|
||||||
@@ -302,45 +291,32 @@
|
|||||||
{@const archive = archiveContext(evidence.snapshot)}
|
{@const archive = archiveContext(evidence.snapshot)}
|
||||||
<li class="record">
|
<li class="record">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<strong>{archive?.player ?? 'Archived message'}</strong
|
<strong>{archive?.player ?? 'Archived conversation'}</strong>
|
||||||
>{#if archive?.round !== null && archive?.round !== undefined}<Badge
|
|
||||||
>Round {archive.round}</Badge
|
|
||||||
>{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if archive?.excerpt}
|
{#if archive?.excerpt}
|
||||||
<EvidenceMessages {...archive.excerpt} />
|
<EvidenceMessages
|
||||||
|
{...archive.excerpt}
|
||||||
|
matchId={archive.matchId}
|
||||||
|
server={archive.server}
|
||||||
|
startedAt={archive.startedAt}
|
||||||
|
/>
|
||||||
{:else}<blockquote>{evidence.text}</blockquote>{/if}
|
{:else}<blockquote>{evidence.text}</blockquote>{/if}
|
||||||
<p class="hint">{evidence.source}</p>
|
{#if archive?.matchId}<a
|
||||||
{#if archive}<div class="source-context">
|
href={contextHref(
|
||||||
{#if archive.matchId}<a
|
`/matches/${archive.matchId}?matchArchive=1`,
|
||||||
href={contextHref(
|
$page.url
|
||||||
`/matches/${archive.matchId}?matchArchive=1`,
|
)}>View match #{archive.matchId}</a
|
||||||
$page.url
|
>{/if}
|
||||||
)}>View match #{archive.matchId}</a
|
|
||||||
>{/if}
|
|
||||||
{#if archive.realTime}<span
|
|
||||||
>Estimated message time · {$timeFormat.dateTime(
|
|
||||||
archive.realTime
|
|
||||||
)}</span
|
|
||||||
>{/if}
|
|
||||||
{#if archive.time}<span
|
|
||||||
>Original log time · {archive.time}</span
|
|
||||||
>{/if}
|
|
||||||
{#if archive.startedAt}<span
|
|
||||||
>Match started · {$timeFormat.dateTime(
|
|
||||||
archive.startedAt
|
|
||||||
)}</span
|
|
||||||
>{/if}
|
|
||||||
</div>{/if}
|
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
Preserved by {evidence.actor} · <RelativeTime
|
Preserved by {evidence.actor} · <RelativeTime
|
||||||
value={evidence.createdAt}
|
value={evidence.createdAt}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
{#if archive}<details>
|
<details>
|
||||||
<summary>Preserved archive snapshot</summary>
|
<summary>Technical details</summary>
|
||||||
<pre>{archive.original}</pre>
|
<p class="hint">{evidence.source}</p>
|
||||||
</details>{/if}
|
<pre>{archive?.original ?? evidence.snapshot}</pre>
|
||||||
|
</details>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -694,15 +670,10 @@
|
|||||||
}
|
}
|
||||||
.hint,
|
.hint,
|
||||||
dt,
|
dt,
|
||||||
.source-context,
|
|
||||||
summary {
|
summary {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
.source-context {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
.notice {
|
.notice {
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ it('selects messages across participants, previews their context, and attaches t
|
|||||||
);
|
);
|
||||||
expect(fetch.mock.calls[0][0]).toContain('game=t5&guid=11');
|
expect(fetch.mock.calls[0][0]).toContain('game=t5&guid=11');
|
||||||
expect(document.querySelector('dialog')?.textContent).toContain('GUID 22');
|
expect(document.querySelector('dialog')?.textContent).toContain('GUID 22');
|
||||||
expect(document.querySelector('dialog')?.textContent).toContain('Message time unknown');
|
expect(document.querySelector('dialog')?.textContent).toContain('Unknown time');
|
||||||
const excerpt = document.querySelector('dialog [aria-label="Preserved conversation excerpt"]')!;
|
const excerpt = document.querySelector('dialog [aria-label="Preserved conversation excerpt"]')!;
|
||||||
expect(excerpt.querySelectorAll('li')).toHaveLength(3);
|
expect(excerpt.querySelectorAll('li')).toHaveLength(3);
|
||||||
expect(excerpt.querySelector('.highlighted')?.textContent).toContain('A concerning reply');
|
expect(excerpt.querySelector('.highlighted')?.textContent).toContain('A concerning reply');
|
||||||
|
|||||||
@@ -205,6 +205,18 @@ it('preserves original evidence context, linked restrictions, and staff history
|
|||||||
expect(document.body.textContent).toContain('Read-only moderation access');
|
expect(document.body.textContent).toContain('Read-only moderation access');
|
||||||
expect(document.body.textContent).toContain('Round 9');
|
expect(document.body.textContent).toContain('Round 9');
|
||||||
expect(document.body.textContent).toContain('Original log time · 12:34');
|
expect(document.body.textContent).toContain('Original log time · 12:34');
|
||||||
|
const evidence = document.querySelector('[aria-labelledby="evidence-title"]')!;
|
||||||
|
expect(
|
||||||
|
evidence.querySelector('[aria-label="Preserved conversation excerpt"]')?.textContent
|
||||||
|
).toContain('abusive message');
|
||||||
|
const technical = [...evidence.querySelectorAll('details')].find(
|
||||||
|
(node) => node.querySelector('summary')?.textContent === 'Technical details'
|
||||||
|
)!;
|
||||||
|
expect(technical.open).toBe(false);
|
||||||
|
expect(technical.textContent).toContain('Archived chat · theater · match 17');
|
||||||
|
expect(JSON.parse(technical.querySelector('pre')!.textContent!)).toEqual(
|
||||||
|
JSON.parse(detail().case.evidence[0].snapshot!)
|
||||||
|
);
|
||||||
expect(document.body.textContent).toContain('Check the earlier warning.');
|
expect(document.body.textContent).toContain('Check the earlier warning.');
|
||||||
expect(document.body.textContent).toContain('Prior warning ignored');
|
expect(document.body.textContent).toContain('Prior warning ignored');
|
||||||
expect(document.body.textContent).toContain('Policy pending');
|
expect(document.body.textContent).toContain('Policy pending');
|
||||||
@@ -223,6 +235,58 @@ it('preserves original evidence context, linked restrictions, and staff history
|
|||||||
expect(document.querySelector('dialog')).toBeNull();
|
expect(document.querySelector('dialog')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('summarizes a preserved conversation across midnight and keeps identifiers on demand', () => {
|
||||||
|
const initial = detail();
|
||||||
|
const archive = JSON.parse(initial.case.evidence[0].snapshot!);
|
||||||
|
archive.entries = [
|
||||||
|
{
|
||||||
|
...archive.entry,
|
||||||
|
messageId: 'subject-message',
|
||||||
|
context: { realTime: '2026-10-16T03:59:00Z', round: 4 },
|
||||||
|
message: { ...archive.entry.message, message: '<b>First message</b>', channel: 'team' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...archive.entry,
|
||||||
|
messageId: 'witness-message',
|
||||||
|
context: { realTime: '2026-10-16T04:01:00Z', round: 9 },
|
||||||
|
message: { ...archive.entry.message, guid: '42', player: 'Witness', message: 'Reply' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...archive.entry,
|
||||||
|
messageId: 'unknown-time',
|
||||||
|
context: { realTime: null, round: null }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
archive.selectedMessageIds = ['subject-message', 'witness-message'];
|
||||||
|
archive.highlightedMessageIds = ['subject-message'];
|
||||||
|
initial.case.evidence[0].snapshot = JSON.stringify(archive);
|
||||||
|
show(initial);
|
||||||
|
const evidence = document.querySelector('[aria-labelledby="evidence-title"]')!;
|
||||||
|
const excerpt = evidence.querySelector('[aria-label="Preserved conversation excerpt"]')!;
|
||||||
|
const rows = excerpt.querySelectorAll('li');
|
||||||
|
expect(rows).toHaveLength(3);
|
||||||
|
expect(rows[0].classList.contains('highlighted')).toBe(true);
|
||||||
|
expect(rows[1].classList.contains('highlighted')).toBe(false);
|
||||||
|
expect(rows[0].textContent).toContain('Team chat');
|
||||||
|
expect(rows[0].textContent).toContain('<b>First message</b>');
|
||||||
|
expect(rows[0].querySelector('b')).toBeNull();
|
||||||
|
expect(rows[1].textContent).toContain('Selected context');
|
||||||
|
expect(rows[2].textContent).toContain('Unknown time');
|
||||||
|
expect(excerpt.textContent).not.toContain(guid);
|
||||||
|
expect(excerpt.textContent).not.toContain('subject-message');
|
||||||
|
expect(excerpt.textContent).not.toContain('Round');
|
||||||
|
const context = excerpt.previousElementSibling!.textContent!;
|
||||||
|
expect(context).toContain('Kino Der Toten · Match #17');
|
||||||
|
expect(context).toContain('Oct 15, 2026 – Oct 16, 2026');
|
||||||
|
expect(context).toContain('Rounds 4–9 (some unknown)');
|
||||||
|
expect(context).toContain('Times are estimates.');
|
||||||
|
const details = excerpt.nextElementSibling as HTMLDetailsElement;
|
||||||
|
expect(details.open).toBe(false);
|
||||||
|
expect(details.textContent).toContain(`GUID ${guid}`);
|
||||||
|
expect(details.textContent).toContain('Message identifier subject-message');
|
||||||
|
expect(details.textContent).toContain('Message identifier witness-message');
|
||||||
|
});
|
||||||
|
|
||||||
it('locks case restrictions to the recorded identity and revision without closing the case', async () => {
|
it('locks case restrictions to the recorded identity and revision without closing the case', async () => {
|
||||||
const updated = detail();
|
const updated = detail();
|
||||||
updated.case.revision = 5;
|
updated.case.revision = 5;
|
||||||
|
|||||||
Reference in New Issue
Block a user