Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/collaboration/CollaborativeCwlEditor.fieldBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as Y from 'yjs';
import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js';
import type { CollaborativeCwlEditorProps } from './types.js';

const COLLABORATION_FIELD_MAX_CODE_UNITS = 1_024;
const INVALID_COLLABORATION_FIELD_MESSAGE =
'Collaboration field must be a string within the supported length.';

afterEach(cleanup);

function captureRenderFailure(field: unknown): unknown {
const consoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
let failure: unknown;
try {
render(
<CollaborativeCwlEditor
document={new Y.Doc()}
field={field as CollaborativeCwlEditorProps['field']}
/>,
);
} catch (error) {
failure = error;
} finally {
consoleError.mockRestore();
}
return failure;
}

describe('CollaborativeCwlEditor field resource boundary', () => {
it('rejects non-string runtime field metadata through a stable redacted error', () => {
expect(captureRenderFailure(42)).toEqual(
new RangeError(INVALID_COLLABORATION_FIELD_MESSAGE),
);
});

it('rejects oversized field metadata before normalization without reflecting it', () => {
const privateMarker = 'private-room-marker';
const field = `${privateMarker}${'x'.repeat(COLLABORATION_FIELD_MAX_CODE_UNITS)}`;
const failure = captureRenderFailure(field);

expect(failure).toEqual(new RangeError(INVALID_COLLABORATION_FIELD_MESSAGE));
expect(String(failure)).not.toContain(privateMarker);
});

it('accepts an in-bound custom field at the local ceiling', () => {
const field = 'x'.repeat(COLLABORATION_FIELD_MAX_CODE_UNITS);

render(<CollaborativeCwlEditor document={new Y.Doc()} field={field} />);

expect(screen.getByRole('status')).toHaveTextContent(
'Collaboration ready · 0 remote collaborators',
);
});
});
60 changes: 60 additions & 0 deletions src/collaboration/CollaborativeCwlEditor.runtimeState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @vitest-environment node

import { renderToString } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js';

describe('collaborative editor runtime state contracts', () => {
it('rejects a non-boolean editable state instead of coercing it into edit authority', () => {
expect(() =>
renderToString(
<CollaborativeCwlEditor
document={new Y.Doc()}
editable={'false' as unknown as boolean}
/>,
),
).toThrowError(
new RangeError('editor editable state must be a boolean when provided'),
);
});

it('rejects a non-boolean toolbar visibility state instead of coercing it', () => {
expect(() =>
renderToString(
<CollaborativeCwlEditor
document={new Y.Doc()}
hideToolbar={'false' as unknown as boolean}
/>,
),
).toThrowError(
new RangeError(
'editor toolbar visibility state must be a boolean when provided',
),
);
});

it('preserves omitted and explicit boolean states', () => {
expect(() =>
renderToString(<CollaborativeCwlEditor document={new Y.Doc()} />),
).not.toThrow();
expect(() =>
renderToString(
<CollaborativeCwlEditor
document={new Y.Doc()}
editable
hideToolbar={false}
/>,
),
).not.toThrow();
expect(() =>
renderToString(
<CollaborativeCwlEditor
document={new Y.Doc()}
editable={false}
hideToolbar
/>,
),
).not.toThrow();
});
});
22 changes: 20 additions & 2 deletions src/collaboration/CollaborativeCwlEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ import {
} from './awareness.js';
import type { CollaborativeCwlEditorProps } from './types.js';

const COLLABORATION_FIELD_MAX_CODE_UNITS = 1_024;
const INVALID_COLLABORATION_FIELD_MESSAGE =
'Collaboration field must be a string within the supported length.';

/**
* Provider-neutral collaborative Inkspan surface backed exclusively by a
* host-owned Yjs document. Inkspan owns neither network nor persistence
Expand Down Expand Up @@ -97,8 +101,23 @@ export const CollaborativeCwlEditor = forwardRef<
ariaRequired,
} = props;

if (typeof editable !== 'boolean') {
throw new RangeError('editor editable state must be a boolean when provided');
}
if (typeof hideToolbar !== 'boolean') {
throw new RangeError(
'editor toolbar visibility state must be a boolean when provided',
);
}
assertCollaborationConfiguration(provider, user);
if (field.trim() === '') {
if (
typeof field !== 'string' ||
field.length > COLLABORATION_FIELD_MAX_CODE_UNITS
) {
throw new RangeError(INVALID_COLLABORATION_FIELD_MESSAGE);
}
const normalizedField = field.trim();
if (normalizedField === '') {
throw new Error('collaboration field must not be empty');
}
if (
Expand All @@ -108,7 +127,6 @@ export const CollaborativeCwlEditor = forwardRef<
throw new Error('collaboration document must be a Y.Doc instance');
}

const normalizedField = field.trim();
const normalizedPlaceholder = useMemo(
() => normalizeEditorPlaceholder(placeholder),
[placeholder],
Expand Down
Loading