Compare commits

..

1 Commits

Author SHA1 Message Date
Anthony LC 2cbd43caae 🔧(y-provider) increase Node.js memory limit
By default, Node.js has a memory limit of
around 512MB, which can lead to out-of-memory
errors when processing large documents.
This commit increases the memory limit to
2GB for the y-provider server, allowing
it to handle larger documents without crashing.
2026-03-25 17:14:27 +01:00
22 changed files with 481 additions and 673 deletions
-3
View File
@@ -15,13 +15,10 @@ and this project adheres to
- 💄(frontend) improve comments highlights #1961
- ♿️(frontend) improve BoxButton a11y and native button semantics #2103
- ♿️(frontend) improve language picker accessibility #2069
- ♿️(frontend) add aria-hidden to decorative icons in dropdown menu #2093
- 🐛(backend) move lock table closer to the insert operation targeted
### Fixed
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
## [v4.8.3] - 2026-03-23
+13 -15
View File
@@ -674,8 +674,17 @@ class DocumentViewSet(
return drf.response.Response(serializer.data)
@transaction.atomic
def perform_create(self, serializer):
"""Set the current user as creator and owner of the newly created object."""
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
# Remove file from validated_data as it's not a model field
# Process it if present
uploaded_file = serializer.validated_data.pop("file", None)
@@ -693,25 +702,15 @@ class DocumentViewSet(
)
serializer.validated_data["content"] = converted_content
serializer.validated_data["title"] = uploaded_file.name
logger.info("conversion ended successfully")
except ConversionError as err:
logger.error("could not convert file content with error: %s", err)
raise drf.exceptions.ValidationError(
{"file": ["Could not convert file content"]}
) from err
with transaction.atomic():
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
obj = models.Document.add_root(
creator=self.request.user,
**serializer.validated_data,
)
obj = models.Document.add_root(
creator=self.request.user,
**serializer.validated_data,
)
serializer.instance = obj
models.DocumentAccess.objects.create(
document=obj,
@@ -828,7 +827,6 @@ class DocumentViewSet(
queryset = self.queryset.filter(path_list)
queryset = queryset.filter(id__in=favorite_documents_ids)
queryset = queryset.filter(ancestors_deleted_at__isnull=True)
queryset = queryset.annotate_user_roles(user)
queryset = queryset.annotate(
is_favorite=db.Value(True, output_field=db.BooleanField())
+11 -10
View File
@@ -267,16 +267,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
# transaction.atomic is used in a context manager to avoid a transaction if
# the settings USER_ONBOARDING_SANDBOX_DOCUMENT is unused
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
try:
template_document = Document.objects.get(id=sandbox_id)
except Document.DoesNotExist:
logger.warning(
"Onboarding sandbox document with id %s does not exist. Skipping.",
sandbox_id,
)
return
with transaction.atomic():
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
@@ -284,6 +274,17 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
f'LOCK TABLE "{Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
try:
template_document = Document.objects.get(id=sandbox_id)
except Document.DoesNotExist:
logger.warning(
"Onboarding sandbox document with id %s does not exist. Skipping.",
sandbox_id,
)
return
sandbox_document = Document.add_root(
title=template_document.title,
content=template_document.content,
@@ -45,8 +45,6 @@ class Converter:
def convert(self, data, content_type, accept):
"""Convert input into other formats using external microservices."""
logger.info("converting content from %s to %s", content_type, accept)
if content_type == mime_types.DOCX and accept == mime_types.YJS:
blocknote_data = self.docspec.convert(
data, mime_types.DOCX, mime_types.BLOCKNOTE
@@ -114,29 +114,3 @@ def test_api_document_favorite_list_with_favorite_children():
assert content[0]["id"] == str(children[0].id)
assert content[1]["id"] == str(children[1].id)
assert content[2]["id"] == str(access.document.id)
def test_api_document_favorite_list_with_deleted_child():
"""
Authenticated users should not see deleted documents in their favorite list.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
root = factories.DocumentFactory(creator=user, users=[user], favorited_by=[user])
child1, child2 = factories.DocumentFactory.create_batch(
2, parent=root, favorited_by=[user]
)
child1.delete()
response = client.get("/api/v1.0/documents/favorite_list/")
assert response.status_code == 200
assert response.json()["count"] == 2
content = response.json()["results"]
assert content[0]["id"] == str(root.id)
assert content[1]["id"] == str(child2.id)
-15
View File
@@ -1,15 +0,0 @@
PORT=3000
BASE_URL=http://localhost:3000
BASE_API_URL=http://localhost:8071/api/v1.0
CUSTOM_SIGN_IN=false
TEST_INSTANCE_ONLY=false
SIGN_IN_EL_TRIGGER=Start Writing
FIRST_NAME=E2E
SIGN_IN_USERNAME_CHROMIUM=user.test@chromium.test
SIGN_IN_USERNAME_WEBKIT=user.test@webkit.com
SIGN_IN_USERNAME_FIREFOX=user.test@firefox.com
# To test server to server API calls
SERVER_TO_SERVER_API_TOKENS='server-api-token'
SUB_CHROMIUM=user.test@chromium.test
SUB_WEBKIT=user.test@webkit.com
SUB_FIREFOX=user.test@firefox.com
-22
View File
@@ -1,22 +0,0 @@
PORT=3000
BASE_URL=http://localhost:3000
BASE_API_URL=http://localhost:8071/api/v1.0
TEST_INSTANCE_ONLY=false
CUSTOM_SIGN_IN=false
SIGN_IN_EL_TRIGGER=Start Writing
FIRST_NAME=E2E
SIGN_IN_USERNAME_CHROMIUM=user.test@chromium.test
SIGN_IN_USERNAME_WEBKIT=user.test@webkit.com
SIGN_IN_USERNAME_FIREFOX=user.test@firefox.com
# Used only on instance with custom sign in
SIGN_IN_EL_USERNAME_INPUT=
SIGN_IN_EL_USERNAME_VALIDATION=
SIGN_IN_EL_PASSWORD_INPUT=
SIGN_IN_PASSWORD_CHROMIUM=
SIGN_IN_PASSWORD_WEBKIT=
SIGN_IN_PASSWORD_FIREFOX=
# To test server to server API calls
SERVER_TO_SERVER_API_TOKENS='server-api-token'
SUB_CHROMIUM=user.test@chromium.test
SUB_WEBKIT=user.test@webkit.com
SUB_FIREFOX=user.test@firefox.com
-1
View File
@@ -5,4 +5,3 @@ blob-report/
playwright/.auth/
playwright/.cache/
screenshots/
.env.local
@@ -1,6 +1,6 @@
import { FullConfig, FullProject, chromium, expect } from '@playwright/test';
import { SignIn } from './utils-signin';
import { keyCloakSignIn } from './utils-common';
const saveStorageState = async (
browserConfig: FullProject<unknown, unknown>,
@@ -22,7 +22,7 @@ const saveStorageState = async (
await page.content();
await expect(page.getByText('Docs').first()).toBeVisible();
await SignIn(page, browserName);
await keyCloakSignIn(page, browserName);
await expect(
page.locator('header').first().getByRole('button', {
@@ -4,166 +4,160 @@ import { expect, test } from '@playwright/test';
import { CONFIG, createDoc, overrideConfig } from './utils-common';
if (process.env.TEST_INSTANCE_ONLY !== 'true') {
test.describe('Config', () => {
test('it checks that sentry is trying to init from config endpoint', async ({
page,
}) => {
await overrideConfig(page, {
SENTRY_DSN: 'https://sentry.io/123',
});
const invalidMsg = 'Invalid Sentry Dsn: https://sentry.io/123';
const consoleMessage = page.waitForEvent('console', {
timeout: 5000,
predicate: (msg) => msg.text().includes(invalidMsg),
});
await page.goto('/');
expect((await consoleMessage).text()).toContain(invalidMsg);
test.describe('Config', () => {
test('it checks that sentry is trying to init from config endpoint', async ({
page,
}) => {
await overrideConfig(page, {
SENTRY_DSN: 'https://sentry.io/123',
});
test('it checks that media server is configured from config endpoint', async ({
page,
browserName,
}) => {
await page.goto('/');
await createDoc(page, 'doc-media', browserName, 1);
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('.bn-block-outer').last().fill('Anything');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toBeVisible();
// Wait for the media-check to be processed
await page.waitForTimeout(1000);
// Check src of image
expect(await image.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
);
const invalidMsg = 'Invalid Sentry Dsn: https://sentry.io/123';
const consoleMessage = page.waitForEvent('console', {
timeout: 5000,
predicate: (msg) => msg.text().includes(invalidMsg),
});
test('it checks that collaboration server is configured from config endpoint', async ({
page,
}) => {
await page.goto('/');
await page.goto('/');
void page
.getByRole('button', {
name: 'New doc',
})
.click();
const webSocket = await page.waitForEvent('websocket', (webSocket) => {
return webSocket
.url()
.includes('ws://localhost:4444/collaboration/ws/');
});
expect(webSocket.url()).toContain(
'ws://localhost:4444/collaboration/ws/',
);
});
test('it checks that Crisp is trying to init from config endpoint', async ({
page,
}) => {
await overrideConfig(page, {
CRISP_WEBSITE_ID: '1234',
});
await page.goto('/');
await expect(
page.locator('#crisp-chatbox').getByText('Invalid website'),
).toBeVisible();
});
test('it checks FRONTEND_CSS_URL config', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
});
await page.goto('/');
await expect(
page
.locator('head link[href="http://localhost:123465/css/style.css"]')
.first(),
).toBeAttached();
});
test('it checks FRONTEND_JS_URL config', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_JS_URL: 'http://localhost:123465/js/script.js',
});
await page.goto('/');
await expect(
page
.locator('script[src="http://localhost:123465/js/script.js"]')
.first(),
).toBeAttached();
});
test('it checks the config api is called', async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/config/') && response.status() === 200,
);
await page.goto('/');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const json = (await response.json()) as typeof CONFIG;
expect(json).toStrictEqual(CONFIG);
});
expect((await consoleMessage).text()).toContain(invalidMsg);
});
test.describe('Config: Not logged', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('it checks that media server is configured from config endpoint', async ({
page,
browserName,
}) => {
await page.goto('/');
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
await page.goto('/');
await createDoc(page, 'doc-media', browserName, 1);
await expect(
page.getByText('Collaborative writing, Simplified.'),
).toHaveCSS('font-family', /Roboto/i, {
timeout: 10000,
});
const fileChooserPromise = page.waitForEvent('filechooser');
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.locator('.bn-block-outer').last().fill('Anything');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
await page.goto('/');
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
await expect(
page.getByText('Collaborative writing, Simplified.'),
).toHaveCSS('font-family', /Marianne/i, {
timeout: 10000,
});
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toBeVisible();
// Wait for the media-check to be processed
await page.waitForTimeout(1000);
// Check src of image
expect(await image.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
);
});
test('it checks that collaboration server is configured from config endpoint', async ({
page,
}) => {
await page.goto('/');
void page
.getByRole('button', {
name: 'New doc',
})
.click();
const webSocket = await page.waitForEvent('websocket', (webSocket) => {
return webSocket.url().includes('ws://localhost:4444/collaboration/ws/');
});
expect(webSocket.url()).toContain('ws://localhost:4444/collaboration/ws/');
});
test('it checks that Crisp is trying to init from config endpoint', async ({
page,
}) => {
await overrideConfig(page, {
CRISP_WEBSITE_ID: '1234',
});
await page.goto('/');
await expect(
page.locator('#crisp-chatbox').getByText('Invalid website'),
).toBeVisible();
});
test('it checks FRONTEND_CSS_URL config', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
});
await page.goto('/');
await expect(
page
.locator('head link[href="http://localhost:123465/css/style.css"]')
.first(),
).toBeAttached();
});
test('it checks FRONTEND_JS_URL config', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_JS_URL: 'http://localhost:123465/js/script.js',
});
await page.goto('/');
await expect(
page
.locator('script[src="http://localhost:123465/js/script.js"]')
.first(),
).toBeAttached();
});
test('it checks the config api is called', async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/config/') && response.status() === 200,
);
await page.goto('/');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const json = (await response.json()) as typeof CONFIG;
expect(json).toStrictEqual(CONFIG);
});
});
test.describe('Config: Not logged', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
await page.goto('/');
await expect(
page.getByText('Collaborative writing, Simplified.'),
).toHaveCSS('font-family', /Roboto/i, {
timeout: 10000,
});
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
await expect(
page.getByText('Collaborative writing, Simplified.'),
).toHaveCSS('font-family', /Marianne/i, {
timeout: 10000,
});
});
}
});
@@ -13,295 +13,210 @@ import {
writeInEditor,
} from './utils-editor';
if (process.env.TEST_INSTANCE_ONLY !== 'true') {
test.describe('Doc AI feature', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
[
{
AI_FEATURE_ENABLED: false,
selector: 'Ask AI',
},
{
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: false,
selector: 'Ask AI',
},
{
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: false,
selector: 'AI',
},
].forEach((config) => {
test(`it checks the AI feature flag from config endpoint: ${JSON.stringify(config)}`, async ({
page,
browserName,
}) => {
await overrideConfig(page, config);
await page.goto('/');
await createDoc(page, 'doc-ai-feature', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').selectText();
await expect(
page.locator('button[data-test="convertMarkdown"]'),
).toHaveCount(1);
await expect(
page.getByRole('button', { name: config.selector, exact: true }),
).toBeHidden();
});
});
test('it checks the AI feature and accepts changes', async ({
test.describe('Doc AI feature', () => {
[
{
AI_FEATURE_ENABLED: false,
selector: 'Ask AI',
},
{
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: false,
selector: 'Ask AI',
},
{
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: false,
selector: 'AI',
},
].forEach((config) => {
test(`it checks the AI feature flag from config endpoint: ${JSON.stringify(config)}`, async ({
page,
browserName,
}) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
await mockAIResponse(page);
await overrideConfig(page, config);
await page.goto('/');
await createDoc(page, 'doc-ai', browserName, 1);
await createDoc(page, 'doc-ai-feature', browserName, 1);
await openSuggestionMenu({ page });
await page.getByText('Ask AI').click();
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').selectText();
await expect(
page.getByRole('option', { name: 'Continue Writing' }),
).toBeVisible();
page.locator('button[data-test="convertMarkdown"]'),
).toHaveCount(1);
await expect(
page.getByRole('option', { name: 'Summarize' }),
).toBeVisible();
page.getByRole('button', { name: config.selector, exact: true }),
).toBeHidden();
});
});
await page.keyboard.press('Escape');
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello World').selectText();
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
await expect(
page.getByRole('option', { name: 'Improve Writing' }),
).toBeVisible();
await expect(
page.getByRole('option', { name: 'Fix Spelling' }),
).toBeVisible();
await expect(
page.getByRole('option', { name: 'Translate' }),
).toBeVisible();
await page.getByRole('option', { name: 'Translate' }).click();
await page
.getByRole('textbox', { name: 'Ask anything...' })
.fill('Translate into french');
await page
.getByRole('textbox', { name: 'Ask anything...' })
.press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Accept')
.click();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
// Check Suggestion menu
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeVisible();
// Reload the page to check that the AI change is still there
await page.goto(page.url());
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
test('it checks the AI feature and accepts changes', async ({
page,
browserName,
}) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
test('it reverts with the AI feature', async ({ page, browserName }) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
await mockAIResponse(page);
await mockAIResponse(page);
await page.goto('/');
await page.goto('/');
await createDoc(page, 'doc-ai', browserName, 1);
await createDoc(page, 'doc-ai', browserName, 1);
await openSuggestionMenu({ page });
await page.getByText('Ask AI').click();
await expect(
page.getByRole('option', { name: 'Continue Writing' }),
).toBeVisible();
await expect(page.getByRole('option', { name: 'Summarize' })).toBeVisible();
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello World').selectText();
await page.keyboard.press('Escape');
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello World').selectText();
await page.getByRole('option', { name: 'Translate' }).click();
await page
.getByRole('textbox', { name: 'Ask anything...' })
.fill('Translate into french');
await page
.getByRole('textbox', { name: 'Ask anything...' })
.press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Revert')
.click();
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
await expect(editor.getByText('Hello World')).toBeVisible();
await expect(
page.getByRole('option', { name: 'Improve Writing' }),
).toBeVisible();
await expect(
page.getByRole('option', { name: 'Fix Spelling' }),
).toBeVisible();
await expect(page.getByRole('option', { name: 'Translate' })).toBeVisible();
await page.getByRole('option', { name: 'Translate' }).click();
await page
.getByRole('textbox', { name: 'Ask anything...' })
.fill('Translate into french');
await page.getByRole('textbox', { name: 'Ask anything...' }).press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Accept')
.click();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
// Check Suggestion menu
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeVisible();
// Reload the page to check that the AI change is still there
await page.goto(page.url());
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
});
test('it reverts with the AI feature', async ({ page, browserName }) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
test('it checks the AI buttons feature legacy', async ({
page,
browserName,
}) => {
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
await route.fulfill({
json: {
answer: 'Hallo Welt',
},
});
} else {
await route.continue();
}
});
await mockAIResponse(page);
await createDoc(page, 'doc-ai', browserName, 1);
await page.goto('/');
await page.locator('.bn-block-outer').last().fill('Hello World');
await createDoc(page, 'doc-ai', browserName, 1);
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello World').selectText();
await page.getByRole('button', { name: 'AI', exact: true }).click();
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Rephrase' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Summarize' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Correct' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
await page.getByRole('option', { name: 'Translate' }).click();
await page
.getByRole('textbox', { name: 'Ask anything...' })
.fill('Translate into french');
await page.getByRole('textbox', { name: 'Ask anything...' }).press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Revert')
.click();
await page.getByRole('menuitem', { name: 'Language' }).hover();
await expect(
page.getByRole('menuitem', { name: 'English', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'French', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'German', exact: true }),
).toBeVisible();
await expect(editor.getByText('Hello World')).toBeVisible();
});
await page.getByRole('menuitem', { name: 'German', exact: true }).click();
await expect(editor.getByText('Hallo Welt')).toBeVisible();
});
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
browserName,
}) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
test('it checks the AI buttons feature legacy', async ({
page,
browserName,
}) => {
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
await route.fulfill({
json: {
answer: 'Hallo Welt',
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
if (!ai_transform && !ai_translate) {
await expect(
page.getByRole('button', { name: 'AI', exact: true }),
).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI', exact: true }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
} else {
await route.continue();
}
});
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
await createDoc(page, 'doc-ai', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'AI', exact: true }).click();
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Rephrase' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Summarize' }),
).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'Language' }).hover();
await expect(
page.getByRole('menuitem', { name: 'English', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'French', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'German', exact: true }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'German', exact: true }).click();
await expect(editor.getByText('Hallo Welt')).toBeVisible();
});
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
browserName,
}) => {
await mockedDocument(page, {
accesses: [
{
@@ -316,7 +231,8 @@ if (process.env.TEST_INSTANCE_ONLY !== 'true') {
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_proxy: false,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
@@ -331,7 +247,7 @@ if (process.env.TEST_INSTANCE_ONLY !== 'true') {
const [randomDoc] = await createDoc(
page,
'doc-editor-ai-proxy',
'doc-editor-ai',
browserName,
1,
);
@@ -343,9 +259,81 @@ if (process.env.TEST_INSTANCE_ONLY !== 'true') {
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeHidden();
if (!ai_transform && !ai_translate) {
await expect(
page.getByRole('button', { name: 'AI', exact: true }),
).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI', exact: true }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
}
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_proxy: false,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai-proxy',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeHidden();
});
});
@@ -58,14 +58,10 @@ test.describe('Doc Comments', () => {
await page.getByRole('button', { name: '👍' }).click();
await expect(
thread
.getByRole('img', { name: `${process.env.FIRST_NAME} ${browserName}` })
.first(),
thread.getByRole('img', { name: `E2E ${browserName}` }).first(),
).toBeVisible();
await expect(thread.getByText('This is a comment').first()).toBeVisible();
await expect(
thread.getByText(`${process.env.FIRST_NAME} ${browserName}`).first(),
).toBeVisible();
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
const urlCommentDoc = page.url();
@@ -89,7 +85,7 @@ test.describe('Doc Comments', () => {
otherThread.getByText('This is a comment').first(),
).toBeVisible();
await expect(
otherThread.getByText(`${process.env.FIRST_NAME} ${browserName}`).first(),
otherThread.getByText(`E2E ${browserName}`).first(),
).toBeVisible();
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
@@ -102,19 +98,13 @@ test.describe('Doc Comments', () => {
// We check that the second user can see the comment he just made
await expect(
otherThread
.getByRole('img', {
name: `${process.env.FIRST_NAME} ${otherBrowserName}`,
})
.first(),
otherThread.getByRole('img', { name: `E2E ${otherBrowserName}` }).first(),
).toBeVisible();
await expect(
otherThread.getByText('This is a comment from the other user').first(),
).toBeVisible();
await expect(
otherThread
.getByText(`${process.env.FIRST_NAME} ${otherBrowserName}`)
.first(),
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
).toBeVisible();
// We check that the first user can see the comment made by the second user in real time
@@ -122,7 +112,7 @@ test.describe('Doc Comments', () => {
thread.getByText('This is a comment from the other user').first(),
).toBeVisible();
await expect(
thread.getByText(`${process.env.FIRST_NAME} ${otherBrowserName}`).first(),
thread.getByText(`E2E ${otherBrowserName}`).first(),
).toBeVisible();
await cleanup();
@@ -144,7 +134,7 @@ test.describe('Doc Comments', () => {
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
await editor.first().click();
@@ -211,7 +201,7 @@ test.describe('Doc Comments', () => {
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
await editor.first().click();
@@ -277,15 +267,11 @@ test.describe('Doc Comments', () => {
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
// We change the role of the second user to reader
await updateRoleUser(
page,
'Reader',
process.env[`SIGN_IN_USERNAME_${otherBrowserName.toUpperCase()}`] || '',
);
await updateRoleUser(page, 'Reader', `user.test@${otherBrowserName}.test`);
// With the reader role, the second user cannot see comments
await otherPage.reload();
@@ -310,15 +296,13 @@ test.describe('Doc Comments', () => {
// Anonymous user can see and add comments
await otherPage.getByRole('button', { name: 'Logout' }).click();
await otherPage.waitForTimeout(1500);
await otherPage.goto(urlCommentDoc);
await verifyDocName(otherPage, docTitle);
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
await otherEditor.getByText('Hello').click();
await expect(
@@ -364,7 +348,7 @@ test.describe('Doc Comments', () => {
await expect(editor1.getByText('Document One')).toHaveCSS(
'background-color',
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
await editor1.getByText('Document One').click();
@@ -3,11 +3,11 @@ import { expect, test } from '@playwright/test';
import {
createDoc,
goToGridDoc,
keyCloakSignIn,
randomName,
verifyDocName,
} from './utils-common';
import { connectOtherUserToDoc } from './utils-share';
import { SignIn } from './utils-signin';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -22,7 +22,8 @@ test.describe('Doc Create', () => {
{ timeout: 5000 },
);
await page.getByRole('button', { name: 'Back to homepage' }).click();
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
@@ -133,7 +134,7 @@ test.describe('Doc Create', () => {
withoutSignIn: true,
});
await SignIn(otherPage, otherBrowserName, false);
await keyCloakSignIn(otherPage, otherBrowserName, false);
await verifyDocName(otherPage, 'From unlogged doc from url');
@@ -159,21 +160,22 @@ test.describe('Doc Create: Not logged', () => {
browserName,
request,
}) => {
const SERVER_TO_SERVER_API_TOKENS = 'server-api-token';
const markdown = `This is a normal text\n\n# And this is a large heading`;
const [title] = randomName('My server way doc create', browserName, 1);
const data = {
title,
content: markdown,
sub: process.env[`SUB_${browserName.toUpperCase()}`],
email: process.env[`SIGN_IN_USERNAME_${browserName.toUpperCase()}`],
sub: `user.test@${browserName}.test`,
email: `user.test@${browserName}.test`,
};
const newDoc = await request.post(
`${process.env.BASE_API_URL}/documents/create-for-owner/`,
`http://localhost:8071/api/v1.0/documents/create-for-owner/`,
{
data,
headers: {
Authorization: `Bearer ${process.env.SERVER_TO_SERVER_API_TOKENS}`,
Authorization: `Bearer ${SERVER_TO_SERVER_API_TOKENS}`,
format: 'json',
},
},
@@ -181,7 +183,7 @@ test.describe('Doc Create: Not logged', () => {
expect(newDoc.ok()).toBeTruthy();
await SignIn(page, browserName);
await keyCloakSignIn(page, browserName);
await goToGridDoc(page, { title });
@@ -208,11 +208,8 @@ export const goToGridDoc = async (
page: Page,
{ nthRow = 1, title }: GoToGridDocOptions = {},
) => {
if (
await page.getByRole('button', { name: 'Back to homepage' }).isVisible()
) {
await page.getByRole('button', { name: 'Back to homepage' }).click();
}
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
@@ -3,9 +3,9 @@ import { Page, chromium, expect } from '@playwright/test';
import {
BrowserName,
getOtherBrowserName,
keyCloakSignIn,
verifyDocName,
} from './utils-common';
import { SignIn } from './utils-signin';
export type Role = 'Administrator' | 'Owner' | 'Editor' | 'Reader';
export type LinkReach = 'Private' | 'Connected' | 'Public';
@@ -131,14 +131,14 @@ export const connectOtherUserToDoc = async ({
.getByRole('main', { name: 'Main content' })
.getByLabel('Login');
const loginFromHome = otherPage.getByRole('button', {
name: process.env.SIGN_IN_EL_TRIGGER,
name: 'Start Writing',
});
await loginFromApp.or(loginFromHome).first().click({
timeout: 15000,
});
await SignIn(otherPage, otherBrowserName, false);
await keyCloakSignIn(otherPage, otherBrowserName, false);
}
if (docTitle) {
await verifyDocName(otherPage, docTitle);
@@ -1,87 +0,0 @@
import { Page, expect } from '@playwright/test';
export const SignIn = async (
page: Page,
browserName: string,
fromHome = true,
) => {
if (process.env.CUSTOM_SIGN_IN === 'true') {
await customSignIn(page, browserName, fromHome);
return;
}
await keyCloakSignIn(page, browserName, fromHome);
};
export const customSignIn = async (
page: Page,
browserName: string,
fromHome = true,
) => {
// Check if already signed in (Silent login or session still valid)
if (
await page
.locator('header')
.first()
.getByRole('button', {
name: 'Logout',
})
.isVisible()
) {
return;
}
if (fromHome) {
await page
.getByRole('button', { name: process.env.SIGN_IN_EL_TRIGGER })
.first()
.click();
}
await page
.getByRole('textbox', { name: process.env.SIGN_IN_EL_USERNAME_INPUT })
.fill(process.env[`SIGN_IN_USERNAME_${browserName.toUpperCase()}`] || '');
if (process.env.SIGN_IN_EL_USERNAME_VALIDATION) {
await page
.getByRole('button', { name: process.env.SIGN_IN_EL_USERNAME_VALIDATION })
.first()
.click();
}
await page
.locator(
`input[name="${process.env.SIGN_IN_EL_PASSWORD_INPUT || 'password'}"]`,
)
.fill(process.env[`SIGN_IN_PASSWORD_${browserName.toUpperCase()}`] || '');
await page.click('button[type="submit"]', { force: true });
};
export const keyCloakSignIn = async (
page: Page,
browserName: string,
fromHome = true,
) => {
if (fromHome) {
await page
.getByRole('button', { name: process.env.SIGN_IN_EL_TRIGGER })
.first()
.click();
}
const login = `user-e2e-${browserName}`;
const password = `password-e2e-${browserName}`;
await expect(
page.locator('.login-pf #kc-header-wrapper').getByText('impress'),
).toBeVisible();
if (await page.getByLabel('Restart login').isVisible()) {
await page.getByLabel('Restart login').click();
}
await page.getByRole('textbox', { name: 'username' }).fill(login);
await page.getByRole('textbox', { name: 'password' }).fill(password);
await page.click('button[type="submit"]', { force: true });
};
-1
View File
@@ -24,7 +24,6 @@
"dependencies": {
"@types/pngjs": "6.0.5",
"convert-stream": "1.0.2",
"dotenv": "17.3.1",
"pdf-parse": "2.4.5",
"pixelmatch": "7.1.0",
"pngjs": "7.0.0"
+2 -4
View File
@@ -1,10 +1,8 @@
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config({ path: ['./.env.local', './.env'], quiet: true, debug: true });
const PORT = process.env.PORT || 3000;
const PORT = process.env.PORT;
const baseURL = process.env.BASE_URL;
const baseURL = `http://localhost:${PORT}`;
/**
* See https://playwright.dev/docs/test-configuration.
@@ -275,6 +275,7 @@ export const DropdownMenu = ({
<Box
$theme="neutral"
$variation={isDisabled ? 'tertiary' : 'primary'}
aria-hidden="true"
>
{option.icon}
</Box>
@@ -118,13 +118,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const options: DropdownMenuOption[] = [
{
label: t('Share'),
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
icon: <GroupSVG width={24} height={24} />,
callback: modalShare.open,
show: isSmallMobile,
},
{
label: t('Export'),
icon: <DownloadSVG width={24} height={24} aria-hidden="true" />,
icon: <DownloadSVG width={24} height={24} />,
callback: () => {
setIsModalExportOpen(true);
},
@@ -133,9 +133,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
{
label: doc.is_favorite ? t('Unpin') : t('Pin'),
icon: doc.is_favorite ? (
<KeepOffSVG width={24} height={24} aria-hidden="true" />
<KeepOffSVG width={24} height={24} />
) : (
<KeepSVG width={24} height={24} aria-hidden="true" />
<KeepSVG width={24} height={24} />
),
callback: () => {
if (doc.is_favorite) {
@@ -148,7 +148,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Version history'),
icon: <HistorySVG width={24} height={24} aria-hidden="true" />,
icon: <HistorySVG width={24} height={24} />,
disabled: !doc.abilities.versions_list,
callback: () => {
selectHistoryModal.open();
@@ -158,7 +158,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Remove emoji'),
icon: <RemoveEmojiSVG width={24} height={24} aria-hidden="true" />,
icon: <RemoveEmojiSVG width={24} height={24} />,
callback: () => {
updateDocEmoji(doc.id, doc.title ?? '', '');
},
@@ -167,7 +167,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Add emoji'),
icon: <AddEmojiSVG width={24} height={24} aria-hidden="true" />,
icon: <AddEmojiSVG width={24} height={24} />,
callback: () => {
updateDocEmoji(doc.id, doc.title ?? '', '📄');
},
@@ -176,12 +176,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Copy link'),
icon: <AddLinkSVG width={24} height={24} aria-hidden="true" />,
icon: <AddLinkSVG width={24} height={24} />,
callback: copyDocLink,
},
{
label: t('Copy as {{format}}', { format: 'Markdown' }),
icon: <MarkdownCopySVG width={24} height={24} aria-hidden="true" />,
icon: <MarkdownCopySVG width={24} height={24} />,
callback: () => {
void copyCurrentEditorToClipboard('markdown');
},
@@ -189,7 +189,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Duplicate'),
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
icon: <ContentCopySVG width={24} height={24} />,
disabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
@@ -202,7 +202,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: isChild ? t('Delete sub-document') : t('Delete document'),
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
icon: <DeleteSVG width={24} height={24} />,
disabled: !doc.abilities.destroy,
callback: () => {
setIsModalRemoveOpen(true);
@@ -72,9 +72,9 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
{
label: doc.is_favorite ? t('Unpin') : t('Pin'),
icon: doc.is_favorite ? (
<KeepOffSVG width={24} height={24} aria-hidden="true" />
<KeepOffSVG width={24} height={24} />
) : (
<KeepSVG width={24} height={24} aria-hidden="true" />
<KeepSVG width={24} height={24} />
),
callback: () => {
if (doc.is_favorite) {
@@ -88,7 +88,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
{
label: t('Share'),
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
icon: <GroupSVG width={24} height={24} />,
callback: () => {
shareModal.open();
},
@@ -97,7 +97,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
{
label: t('Move into a doc'),
icon: <DocMoveInSVG width={24} height={24} aria-hidden="true" />,
icon: <DocMoveInSVG width={24} height={24} />,
callback: () => {
importModal.open();
},
@@ -106,7 +106,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
{
label: t('Duplicate'),
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
icon: <ContentCopySVG width={24} height={24} />,
disabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
@@ -119,7 +119,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
{
label: t('Delete'),
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
icon: <DeleteSVG width={24} height={24} />,
callback: () => deleteModal.open(),
disabled: !doc.abilities.destroy,
testId: `docs-grid-actions-remove-${doc.id}`,
+15 -13
View File
@@ -1769,11 +1769,18 @@
minimatch "^10.2.4"
"@eslint/config-helpers@^0.5.2":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.3.tgz#721fe6bbb90d74b0c80d6ff2428e5bbcb002becb"
integrity sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==
version "0.5.2"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.2.tgz#314c7b03d02a371ad8c0a7f6821d5a8a8437ba9d"
integrity sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==
dependencies:
"@eslint/core" "^1.1.1"
"@eslint/core" "^1.1.0"
"@eslint/core@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.1.0.tgz#51f5cd970e216fbdae6721ac84491f57f965836d"
integrity sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/core@^1.1.1":
version "1.1.1"
@@ -10344,9 +10351,9 @@ eslint@10.0.3:
optionator "^0.9.3"
espree@^11.1.1:
version "11.2.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-11.2.0.tgz#01d5e47dc332aaba3059008362454a8cc34ccaa5"
integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==
version "11.1.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-11.1.1.tgz#866f6bc9ccccd6f28876b7a6463abb281b9cb847"
integrity sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==
dependencies:
acorn "^8.16.0"
acorn-jsx "^5.3.2"
@@ -10722,12 +10729,7 @@ flat-cache@^6.1.20:
flatted "^3.3.3"
hookified "^1.15.0"
flatted@^3.2.9:
version "3.4.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
flatted@^3.3.3:
flatted@^3.2.9, flatted@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==