mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2083eaddd6 | ||
|
|
d398ebc44b | ||
|
|
59887ad404 | ||
|
|
5aa283e8e0 | ||
|
|
d9ed256a4b | ||
|
|
a297f89b83 | ||
|
|
4a16921242 |
No files matched your search
@@ -3,7 +3,3 @@
|
||||
.nativeDragRegion {
|
||||
-webkit-app-region: none;
|
||||
}
|
||||
|
||||
:global(html.platform-native.platform-macos) .nativeDragRegion {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
buildMaskedLink,
|
||||
canWrapSelectionAsLink,
|
||||
parsePastedUrl,
|
||||
} from '@app/features/lexical/composer/ComposerLinkPaste';
|
||||
import {resolvePastedLinkInsertion} from '@app/features/lexical/composer/ComposerLinkPaste';
|
||||
import {
|
||||
$createComposerSegmentNodes,
|
||||
$projectComposer,
|
||||
@@ -13,6 +9,7 @@ import {
|
||||
import {$captureSelectionOffsets, $selectComposerOffset} from '@app/features/lexical/composer/composerOffsets';
|
||||
import {$isSlashSlotNode, type SlashSlotNode} from '@app/features/lexical/composer/nodes/SlashSlotNode';
|
||||
import {COMPOSER_SLASH_SLOT_STATE_MAX_ID_LENGTH} from '@app/features/lexical/composer/SlashSlotPersistence';
|
||||
import {ParserFlags} from '@app/features/messaging/utils/markdown/parser/Enums';
|
||||
import type {MentionSegment} from '@app/features/messaging/utils/TextareaSegmentManager';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
@@ -26,6 +23,7 @@ import {
|
||||
$isNodeSelection,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
COPY_COMMAND,
|
||||
CUT_COMMAND,
|
||||
@@ -65,6 +63,7 @@ export interface ComposerClipboardSelection extends ComposerClipboardSlice {
|
||||
export interface ComposerClipboardCommandState {
|
||||
getPlainText(): boolean;
|
||||
isEditable(): boolean;
|
||||
getMarkdownParserFlags(): number;
|
||||
}
|
||||
|
||||
interface SerializedComposerClipboardPayload {
|
||||
@@ -468,31 +467,30 @@ export function registerComposerClipboardCommands(
|
||||
editor.registerCommand(
|
||||
PASTE_COMMAND,
|
||||
(event) => {
|
||||
if (!state.isEditable() || state.getPlainText()) {
|
||||
if (!state.isEditable() || (state.getMarkdownParserFlags() & ParserFlags.ALLOW_MASKED_LINKS) === 0) {
|
||||
return false;
|
||||
}
|
||||
const clipboardEvent = getClipboardEvent(event);
|
||||
if (clipboardEvent == null || clipboardEvent.clipboardData == null) {
|
||||
return false;
|
||||
}
|
||||
const url = parsePastedUrl(clipboardEvent.clipboardData.getData('text/plain'));
|
||||
if (url == null) {
|
||||
return false;
|
||||
}
|
||||
const selection = $getSelection();
|
||||
if (!$isRangeSelection(selection) || selection.isCollapsed()) {
|
||||
return false;
|
||||
}
|
||||
const selectedText = selection.getTextContent();
|
||||
if (!canWrapSelectionAsLink(selectedText)) {
|
||||
const replacement = resolvePastedLinkInsertion(
|
||||
clipboardEvent.clipboardData.getData('text/plain'),
|
||||
selection.getTextContent(),
|
||||
);
|
||||
if (replacement === null) {
|
||||
return false;
|
||||
}
|
||||
selection.insertText(buildMaskedLink(selectedText, url));
|
||||
selection.insertText(replacement);
|
||||
$addUpdateTag(PASTE_TAG);
|
||||
clipboardEvent.preventDefault();
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
),
|
||||
editor.registerCommand(
|
||||
COPY_COMMAND,
|
||||
|
||||
@@ -4,7 +4,12 @@ import {
|
||||
buildMaskedLink,
|
||||
canWrapSelectionAsLink,
|
||||
parsePastedUrl,
|
||||
resolvePastedLinkInsertion,
|
||||
} from '@app/features/lexical/composer/ComposerLinkPaste';
|
||||
import {DEFAULT_COMPOSER_MARKDOWN_FLAGS} from '@app/features/lexical/composer/markdownSpans';
|
||||
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
|
||||
import {getParserFlagsForContext} from '@app/features/messaging/utils/markdown/MarkdownParserFlags';
|
||||
import {ParserFlags} from '@app/features/messaging/utils/markdown/parser/Enums';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('parsePastedUrl', () => {
|
||||
@@ -60,3 +65,42 @@ describe('buildMaskedLink', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePastedLinkInsertion', () => {
|
||||
it('wraps a selection when a bare url is pasted over it', () => {
|
||||
expect(resolvePastedLinkInsertion('https://fluxer.app/download', 'the download page')).toBe(
|
||||
'[the download page](<https://fluxer.app/download>)',
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps a message link the same way a plain url is wrapped', () => {
|
||||
const messageLink = 'https://fluxer.app/channels/1234567890/9876543210/1122334455';
|
||||
expect(resolvePastedLinkInsertion(messageLink, 'this message')).toBe(`[this message](<${messageLink}>)`);
|
||||
});
|
||||
|
||||
it('declines when the pasted text is not a url', () => {
|
||||
expect(resolvePastedLinkInsertion('just some text', 'selected')).toBeNull();
|
||||
});
|
||||
|
||||
it('declines when the selection cannot be wrapped', () => {
|
||||
expect(resolvePastedLinkInsertion('https://fluxer.app', ' ')).toBeNull();
|
||||
expect(resolvePastedLinkInsertion('https://fluxer.app', 'https://example.test')).toBeNull();
|
||||
});
|
||||
|
||||
it('declines a non-web protocol so pasting cannot forge a link', () => {
|
||||
expect(resolvePastedLinkInsertion('javascript:alert(1)', 'click me')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('masked link support across composer surfaces', () => {
|
||||
const surfaces: Array<[string, number]> = [
|
||||
['channel composer', DEFAULT_COMPOSER_MARKDOWN_FLAGS],
|
||||
['message edit / forward', getParserFlagsForContext(MarkdownContext.STANDARD_WITH_JUMBO)],
|
||||
['channel topic', getParserFlagsForContext(MarkdownContext.STANDARD_WITHOUT_JUMBO)],
|
||||
['user bio', getParserFlagsForContext(MarkdownContext.RESTRICTED_USER_BIO)],
|
||||
];
|
||||
|
||||
it.each(surfaces)('%s allows masked links, so pasting a link must wrap', (_name, flags) => {
|
||||
expect(flags & ParserFlags.ALLOW_MASKED_LINKS).not.toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -43,3 +43,11 @@ export function canWrapSelectionAsLink(selectedText: string): boolean {
|
||||
export function buildMaskedLink(selectedText: string, url: string): string {
|
||||
return `[${selectedText}](<${url}>)`;
|
||||
}
|
||||
|
||||
export function resolvePastedLinkInsertion(pastedText: string, selectedText: string): string | null {
|
||||
const url = parsePastedUrl(pastedText);
|
||||
if (url === null || !canWrapSelectionAsLink(selectedText)) {
|
||||
return null;
|
||||
}
|
||||
return buildMaskedLink(selectedText, url);
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
$wrapComposerSelection,
|
||||
} from '@app/features/lexical/composer/composerOffsets';
|
||||
import styles from '@app/features/lexical/composer/LexicalMessageComposer.module.css';
|
||||
import {DEFAULT_COMPOSER_MARKDOWN_FLAGS} from '@app/features/lexical/composer/markdownSpans';
|
||||
import {ComposerCommandNode} from '@app/features/lexical/composer/nodes/ComposerCommandNode';
|
||||
import {ComposerCustomEmojiNode} from '@app/features/lexical/composer/nodes/ComposerCustomEmojiNode';
|
||||
import {ComposerMentionNode} from '@app/features/lexical/composer/nodes/ComposerMentionNode';
|
||||
@@ -263,6 +264,8 @@ const ComposerInner = ({
|
||||
submitOnEnterRef.current = submitOnEnter;
|
||||
const plainTextRef = useRef(plainText);
|
||||
plainTextRef.current = plainText;
|
||||
const markdownParserFlagsRef = useRef(markdownParserFlags ?? DEFAULT_COMPOSER_MARKDOWN_FLAGS);
|
||||
markdownParserFlagsRef.current = markdownParserFlags ?? DEFAULT_COMPOSER_MARKDOWN_FLAGS;
|
||||
const disabledRef = useRef(disabled);
|
||||
disabledRef.current = disabled;
|
||||
const onSlashCommandStateChangeRef = useRef(onSlashCommandStateChange);
|
||||
@@ -607,6 +610,7 @@ const ComposerInner = ({
|
||||
registerComposerClipboardCommands(editor, {
|
||||
getPlainText: () => plainTextRef.current,
|
||||
isEditable: () => !disabledRef.current && editor.isEditable(),
|
||||
getMarkdownParserFlags: () => markdownParserFlagsRef.current,
|
||||
}),
|
||||
[editor],
|
||||
);
|
||||
|
||||
+1
@@ -156,6 +156,7 @@ export const NativeTitleBarControl = observer(() => {
|
||||
},
|
||||
[i18n, updateDesktopWindowBehavior],
|
||||
);
|
||||
if (getElectronAPI()?.platform === 'darwin') return null;
|
||||
return (
|
||||
<Switch
|
||||
ariaLabel={i18n._(USE_NATIVE_TITLE_BAR_DESCRIPTOR)}
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import {CLOSE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {getElectronAPI, isDesktop} from '@app/features/ui/utils/NativeUtils';
|
||||
import {getElectronAPI, getNativePlatformSync, isDesktop, isNativeMacOS} from '@app/features/ui/utils/NativeUtils';
|
||||
import type {SearchableSettingDescriptor} from '@app/features/user/components/settings_utils/search_index/SearchIndexTypes';
|
||||
import {BACKGROUND_DESCRIPTOR} from '@app/features/user/components/settings_utils/search_index/SharedDescriptors';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
@@ -283,7 +283,7 @@ export const desktopSettingsIndex: Array<SearchableSettingDescriptor> = [
|
||||
],
|
||||
description: USE_THE_OPERATING_SYSTEM_S_WINDOW_CHROME_DESCRIPTOR,
|
||||
tags: ['desktop', 'appearance'],
|
||||
isVisible: isDesktop,
|
||||
isVisible: () => isDesktop() && !isNativeMacOS(getNativePlatformSync()),
|
||||
},
|
||||
{
|
||||
id: 'advanced-tray-icon',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getDesktopWindowBehaviorSettings} from '@app/features/ui/utils/DesktopWindowBehaviorUtils';
|
||||
import {isDesktop} from '@app/features/ui/utils/NativeUtils';
|
||||
import {getNativePlatformSync, isDesktop, isNativeMacOS} from '@app/features/ui/utils/NativeUtils';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
const TITLEBAR_HEIGHT_CSS_VAR = '--native-titlebar-height';
|
||||
@@ -9,8 +9,12 @@ const CUSTOM_TITLEBAR_HEIGHT = '32px';
|
||||
const NATIVE_TITLEBAR_HEIGHT = '0px';
|
||||
const NATIVE_SYSTEM_TITLEBAR_CLASS = 'native-system-titlebar';
|
||||
|
||||
function supportsSystemTitleBar(): boolean {
|
||||
return isDesktop() && !isNativeMacOS(getNativePlatformSync());
|
||||
}
|
||||
|
||||
function getInitialUseSystemTitleBar(): boolean {
|
||||
return isDesktop() && document.documentElement.classList.contains(NATIVE_SYSTEM_TITLEBAR_CLASS);
|
||||
return supportsSystemTitleBar() && document.documentElement.classList.contains(NATIVE_SYSTEM_TITLEBAR_CLASS);
|
||||
}
|
||||
|
||||
export function useNativeTitleBar(): boolean {
|
||||
@@ -19,6 +23,10 @@ export function useNativeTitleBar(): boolean {
|
||||
if (!isDesktop()) {
|
||||
return;
|
||||
}
|
||||
if (!supportsSystemTitleBar()) {
|
||||
setUseSystemTitleBar(false);
|
||||
return;
|
||||
}
|
||||
let mounted = true;
|
||||
void getDesktopWindowBehaviorSettings().then((settings) => {
|
||||
if (!mounted || !settings) {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "24.10.9",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260130.1",
|
||||
"electron": "43.4.1",
|
||||
"electron": "43.4.0",
|
||||
"electron-builder": "26.9.0",
|
||||
"esbuild": "0.27.2",
|
||||
"typescript": "5.9.3"
|
||||
|
||||
@@ -128,7 +128,7 @@ function consumeWaylandPortalSurfacePreference(): DisplayMediaPortalSurfacePrefe
|
||||
return preference;
|
||||
}
|
||||
|
||||
export const DISPLAY_MEDIA_PORTAL_EMPTY_CHANNEL = 'display-media-portal-empty';
|
||||
const DISPLAY_MEDIA_PORTAL_EMPTY_CHANNEL = 'display-media-portal-empty';
|
||||
|
||||
type WaylandPortalUnavailableReason = 'empty' | 'error';
|
||||
|
||||
@@ -152,7 +152,7 @@ async function defaultWaylandPortalSourceProvider(
|
||||
return desktopCapturer.getSources({types});
|
||||
}
|
||||
|
||||
export async function resolveWaylandPortalDisplayMedia(
|
||||
async function resolveWaylandPortalDisplayMedia(
|
||||
context: WaylandPortalResolutionContext,
|
||||
): Promise<Electron.Streams | null> {
|
||||
const {requestId, preference, getSources, notifyUnavailable} = context;
|
||||
|
||||
@@ -148,6 +148,31 @@ function buildDesktopFileContents(execPath: string, hidden: boolean): string {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function readDesktopEntryValue(contents: string, key: string): string | null {
|
||||
for (const line of contents.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('[Desktop Action ')) break;
|
||||
if (trimmed.startsWith(`${key}=`)) return trimmed.slice(key.length + 1).trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isExecutableFile(candidate: string): boolean {
|
||||
try {
|
||||
if (!fs.statSync(candidate).isFile()) return false;
|
||||
fs.accessSync(candidate, fs.constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isStaleDesktopFile(contents: string): boolean {
|
||||
const tryExec = readDesktopEntryValue(contents, 'TryExec');
|
||||
if (tryExec === null || !tryExec.startsWith('/')) return false;
|
||||
return !isExecutableFile(tryExec);
|
||||
}
|
||||
|
||||
function readExistingDesktopFile(filePath: string): string | null {
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
@@ -213,10 +238,29 @@ export function ensureLinuxProtocolDesktopEntry(): void {
|
||||
}
|
||||
if (existing !== null) {
|
||||
if (!existing.includes(GENERATED_MARKER)) {
|
||||
logger.debug('Linux .desktop entry was hand-edited; leaving untouched', {filePath});
|
||||
return;
|
||||
if (!isStaleDesktopFile(existing)) {
|
||||
logger.debug('Linux .desktop entry was hand-edited; leaving untouched', {filePath});
|
||||
return;
|
||||
}
|
||||
const systemEntry = findSystemDesktopEntry();
|
||||
if (systemEntry) {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
logger.info('Removed stale .desktop entry shadowing the system entry', {filePath, systemEntry});
|
||||
} catch (error) {
|
||||
logger.warn('Failed to remove stale .desktop entry', {filePath, error});
|
||||
}
|
||||
try {
|
||||
app.setAsDefaultProtocolClient(APP_PROTOCOL);
|
||||
} catch (error) {
|
||||
logger.warn('Failed to register protocol client', {error});
|
||||
}
|
||||
return;
|
||||
}
|
||||
logger.info('Rewriting stale .desktop entry whose TryExec no longer resolves', {filePath});
|
||||
} else {
|
||||
needsWrite = existing !== desired;
|
||||
}
|
||||
needsWrite = existing !== desired;
|
||||
}
|
||||
if (!needsWrite) {
|
||||
logger.debug('Linux .desktop entry already up to date', {filePath});
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
interface FakeCommandLineState {
|
||||
switches: Map<string, string>;
|
||||
}
|
||||
|
||||
function createFakeCommandLine(initial: Record<string, string | true> = {}) {
|
||||
const state: FakeCommandLineState = {switches: new Map()};
|
||||
for (const [name, value] of Object.entries(initial)) {
|
||||
state.switches.set(name, value === true ? '' : value);
|
||||
}
|
||||
return {
|
||||
state,
|
||||
appendSwitch(name: string, value?: string): void {
|
||||
state.switches.set(name, value ?? '');
|
||||
},
|
||||
getSwitchValue(name: string): string {
|
||||
return state.switches.get(name) ?? '';
|
||||
},
|
||||
hasSwitch(name: string): boolean {
|
||||
return state.switches.has(name);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const electronCommandLine = createFakeCommandLine();
|
||||
|
||||
test.mock.module('electron', {
|
||||
namedExports: {
|
||||
app: {commandLine: electronCommandLine},
|
||||
desktopCapturer: {getSources: async () => []},
|
||||
ipcMain: {handle: () => {}, on: () => {}},
|
||||
screen: {getAllDisplays: () => []},
|
||||
BrowserWindow: {getAllWindows: () => []},
|
||||
},
|
||||
});
|
||||
|
||||
test.mock.module('electron-log', {
|
||||
defaultExport: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const {addLinuxScreenCapturePipeWireFeature, appendLinuxOzonePlatformHint, hasConfiguredOzonePlatformSwitch} =
|
||||
await import('./ChromiumRuntime');
|
||||
|
||||
const {resolveWaylandPortalDisplayMedia, DISPLAY_MEDIA_PORTAL_EMPTY_CHANNEL} = await import('./DisplayMedia');
|
||||
|
||||
function withPlatform<T>(platform: NodeJS.Platform, run: () => T): T {
|
||||
const original = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
Object.defineProperty(process, 'platform', {configurable: true, value: platform});
|
||||
try {
|
||||
return run();
|
||||
} finally {
|
||||
if (original) {
|
||||
Object.defineProperty(process, 'platform', original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('addLinuxScreenCapturePipeWireFeature enables WebRTCPipeWireCapturer only on Linux', () => {
|
||||
withPlatform('linux', () => {
|
||||
const features = new Set<string>();
|
||||
addLinuxScreenCapturePipeWireFeature(features);
|
||||
assert.ok(features.has('WebRTCPipeWireCapturer'));
|
||||
});
|
||||
withPlatform('darwin', () => {
|
||||
const features = new Set<string>();
|
||||
addLinuxScreenCapturePipeWireFeature(features);
|
||||
assert.equal(features.has('WebRTCPipeWireCapturer'), false);
|
||||
});
|
||||
withPlatform('win32', () => {
|
||||
const features = new Set<string>();
|
||||
addLinuxScreenCapturePipeWireFeature(features);
|
||||
assert.equal(features.has('WebRTCPipeWireCapturer'), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('appendLinuxOzonePlatformHint appends ozone-platform-hint=auto on Linux', () => {
|
||||
withPlatform('linux', () => {
|
||||
const commandLine = createFakeCommandLine();
|
||||
appendLinuxOzonePlatformHint(commandLine);
|
||||
assert.equal(commandLine.getSwitchValue('ozone-platform-hint'), 'auto');
|
||||
assert.ok(commandLine.hasSwitch('ozone-platform-hint'));
|
||||
});
|
||||
});
|
||||
|
||||
test('appendLinuxOzonePlatformHint does not append on darwin or win32', () => {
|
||||
withPlatform('darwin', () => {
|
||||
const commandLine = createFakeCommandLine();
|
||||
appendLinuxOzonePlatformHint(commandLine);
|
||||
assert.equal(commandLine.hasSwitch('ozone-platform-hint'), false);
|
||||
});
|
||||
withPlatform('win32', () => {
|
||||
const commandLine = createFakeCommandLine();
|
||||
appendLinuxOzonePlatformHint(commandLine);
|
||||
assert.equal(commandLine.hasSwitch('ozone-platform-hint'), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('appendLinuxOzonePlatformHint does not override a user-supplied --ozone-platform', () => {
|
||||
withPlatform('linux', () => {
|
||||
const commandLine = createFakeCommandLine({'ozone-platform': 'x11'});
|
||||
assert.ok(hasConfiguredOzonePlatformSwitch(commandLine));
|
||||
appendLinuxOzonePlatformHint(commandLine);
|
||||
assert.equal(commandLine.hasSwitch('ozone-platform-hint'), false);
|
||||
assert.equal(commandLine.getSwitchValue('ozone-platform'), 'x11');
|
||||
});
|
||||
});
|
||||
|
||||
test('appendLinuxOzonePlatformHint respects a user-supplied bare --ozone-platform switch', () => {
|
||||
withPlatform('linux', () => {
|
||||
const commandLine = createFakeCommandLine({'ozone-platform': true});
|
||||
assert.ok(hasConfiguredOzonePlatformSwitch(commandLine));
|
||||
appendLinuxOzonePlatformHint(commandLine);
|
||||
assert.equal(commandLine.hasSwitch('ozone-platform-hint'), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveWaylandPortalDisplayMedia returns the first source when the portal provides one', async () => {
|
||||
const source = {id: 'screen:0:0', name: 'Screen 1'} as Electron.DesktopCapturerSource;
|
||||
const notifications: Array<{requestId: string; reason: string}> = [];
|
||||
const streams = await resolveWaylandPortalDisplayMedia({
|
||||
requestId: 'req-1',
|
||||
preference: 'monitor',
|
||||
getSources: async () => [source],
|
||||
notifyUnavailable: (requestId, reason) => notifications.push({requestId, reason}),
|
||||
});
|
||||
assert.deepEqual(streams, {video: source});
|
||||
assert.equal(notifications.length, 0);
|
||||
});
|
||||
|
||||
test('resolveWaylandPortalDisplayMedia signals an empty portal result without a video stream', async () => {
|
||||
const notifications: Array<{requestId: string; reason: string}> = [];
|
||||
const streams = await resolveWaylandPortalDisplayMedia({
|
||||
requestId: 'req-2',
|
||||
preference: 'monitor',
|
||||
getSources: async () => [],
|
||||
notifyUnavailable: (requestId, reason) => notifications.push({requestId, reason}),
|
||||
});
|
||||
assert.equal(streams, null);
|
||||
assert.deepEqual(notifications, [{requestId: 'req-2', reason: 'empty'}]);
|
||||
});
|
||||
|
||||
test('resolveWaylandPortalDisplayMedia signals a portal error when getSources rejects', async () => {
|
||||
const notifications: Array<{requestId: string; reason: string}> = [];
|
||||
const streams = await resolveWaylandPortalDisplayMedia({
|
||||
requestId: 'req-3',
|
||||
preference: 'window',
|
||||
getSources: async () => {
|
||||
throw new Error('xdg-desktop-portal unavailable');
|
||||
},
|
||||
notifyUnavailable: (requestId, reason) => notifications.push({requestId, reason}),
|
||||
});
|
||||
assert.equal(streams, null);
|
||||
assert.deepEqual(notifications, [{requestId: 'req-3', reason: 'error'}]);
|
||||
});
|
||||
|
||||
test('the Wayland portal-unavailable IPC channel matches the wired renderer channel', () => {
|
||||
assert.equal(DISPLAY_MEDIA_PORTAL_EMPTY_CHANNEL, 'display-media-portal-empty');
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
isFluxerGameCaptureVulkanLayerValue,
|
||||
shouldRemoveStaleFluxerGameCaptureVulkanLayerValue,
|
||||
} from './WindowsVulkanGameCaptureLayer';
|
||||
|
||||
const INSTALLED_MANIFEST_PATH =
|
||||
'C:\\Users\\hampus\\AppData\\Local\\fluxer_desktop_canary\\current\\resources\\app.asar.unpacked\\node_modules\\@fluxer\\win-game-capture\\fluxer-vulkan-layer.win32-arm64-msvc.json';
|
||||
const DEV_TREE_MANIFEST_PATH =
|
||||
'C:\\t\\fluxer\\fluxer_desktop\\native\\win-game-capture\\fluxer-vulkan-layer.win32-arm64-msvc.json';
|
||||
const RETIRED_PACKAGE_MANIFEST_PATH =
|
||||
'C:\\Users\\hampus\\AppData\\Local\\fluxer_desktop\\current\\resources\\app.asar.unpacked\\node_modules\\@fluxer\\win-screen-capture\\fluxer-vulkan-layer.win32-arm64-msvc.json';
|
||||
|
||||
test('isFluxerGameCaptureVulkanLayerValue matches the installed @fluxer/win-game-capture manifest', () => {
|
||||
assert.ok(isFluxerGameCaptureVulkanLayerValue(INSTALLED_MANIFEST_PATH));
|
||||
});
|
||||
|
||||
test('isFluxerGameCaptureVulkanLayerValue matches a dev/build tree manifest outside node_modules/@fluxer', () => {
|
||||
assert.ok(isFluxerGameCaptureVulkanLayerValue(DEV_TREE_MANIFEST_PATH));
|
||||
});
|
||||
|
||||
test('isFluxerGameCaptureVulkanLayerValue matches the retired win-screen-capture package name', () => {
|
||||
assert.ok(isFluxerGameCaptureVulkanLayerValue(RETIRED_PACKAGE_MANIFEST_PATH));
|
||||
});
|
||||
|
||||
test('isFluxerGameCaptureVulkanLayerValue matches every supported architecture and registry slash style', () => {
|
||||
for (const arch of ['x64', 'ia32', 'arm64']) {
|
||||
assert.ok(
|
||||
isFluxerGameCaptureVulkanLayerValue(
|
||||
`C:\\t\\fluxer\\native\\win-game-capture\\fluxer-vulkan-layer.win32-${arch}-msvc.json`,
|
||||
),
|
||||
arch,
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
isFluxerGameCaptureVulkanLayerValue(
|
||||
'C:/t/fluxer/fluxer_desktop/native/win-game-capture/Fluxer-Vulkan-Layer.win32-ARM64-msvc.json',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('isFluxerGameCaptureVulkanLayerValue never matches another vendor Vulkan implicit layer', () => {
|
||||
const foreignValues = [
|
||||
'C:\\Program Files\\NVIDIA Corporation\\Nsight Graphics\\nvoglv64.json',
|
||||
'C:\\Program Files (x86)\\Steam\\steamoverlayvulkanlayer.json',
|
||||
'C:\\Program Files\\obs-studio\\data\\obs-plugins\\win-capture\\obs-vulkan64.json',
|
||||
'C:\\Windows\\System32\\VkLayer_khronos_validation.json',
|
||||
'C:\\Program Files\\OtherVendor\\layers\\fluxer-vulkan-layer.win32-x64-msvc.json',
|
||||
'C:\\t\\fluxer\\fluxer_desktop\\native\\win-game-capture\\othervendor-vulkan-layer.json',
|
||||
'C:\\t\\fluxer\\fluxer_desktop\\native\\linux-screen-capture\\fluxer-vulkan-layer.win32-x64-msvc.json',
|
||||
'C:\\t\\fluxer\\fluxer_desktop\\native\\mac-screen-capture\\fluxer-vulkan-layer.win32-x64-msvc.json',
|
||||
'C:\\t\\fluxer\\fluxer_desktop\\native\\win-game-capture\\fluxer-vulkan-layer.win32-arm64-msvc.dll',
|
||||
];
|
||||
for (const valueName of foreignValues) {
|
||||
assert.equal(isFluxerGameCaptureVulkanLayerValue(valueName), false, valueName);
|
||||
}
|
||||
});
|
||||
|
||||
test('shouldRemoveStaleFluxerGameCaptureVulkanLayerValue keeps the live registration of the running install', () => {
|
||||
assert.equal(
|
||||
shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(INSTALLED_MANIFEST_PATH, INSTALLED_MANIFEST_PATH),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(
|
||||
INSTALLED_MANIFEST_PATH.replace(/\\/g, '/').toUpperCase(),
|
||||
INSTALLED_MANIFEST_PATH,
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldRemoveStaleFluxerGameCaptureVulkanLayerValue removes orphaned registrations alongside the kept one', () => {
|
||||
assert.ok(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(DEV_TREE_MANIFEST_PATH, INSTALLED_MANIFEST_PATH));
|
||||
assert.ok(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(RETIRED_PACKAGE_MANIFEST_PATH, INSTALLED_MANIFEST_PATH));
|
||||
});
|
||||
|
||||
test('shouldRemoveStaleFluxerGameCaptureVulkanLayerValue removes every Fluxer layer when nothing is kept', () => {
|
||||
for (const valueName of [INSTALLED_MANIFEST_PATH, DEV_TREE_MANIFEST_PATH, RETIRED_PACKAGE_MANIFEST_PATH]) {
|
||||
assert.ok(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(valueName, null), valueName);
|
||||
}
|
||||
});
|
||||
|
||||
test('shouldRemoveStaleFluxerGameCaptureVulkanLayerValue never removes a foreign vendor layer, kept path or not', () => {
|
||||
const foreignValue = 'C:\\Program Files (x86)\\Steam\\steamoverlayvulkanlayer.json';
|
||||
assert.equal(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(foreignValue, null), false);
|
||||
assert.equal(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(foreignValue, INSTALLED_MANIFEST_PATH), false);
|
||||
});
|
||||
@@ -1170,6 +1170,29 @@ pub(crate) fn is_block_start(line: &str, flags: u32) -> bool {
|
||||
|| is_blockquote_start(line, flags)
|
||||
}
|
||||
|
||||
pub(crate) fn is_table_start(lines: &[Line], index: usize, flags: u32) -> bool {
|
||||
if !ParserFlags::has(flags, ParserFlags::ALLOW_TABLES) || index + 2 >= lines.len() {
|
||||
return false;
|
||||
}
|
||||
let header_line = trim_table_line(&lines[index]);
|
||||
let align_line = trim_table_line(&lines[index + 1]);
|
||||
if !header_line.text.contains('|') || !align_line.text.contains('|') {
|
||||
return false;
|
||||
}
|
||||
let header_cells = split_table_cells(&header_line);
|
||||
if header_cells.is_empty() || !cells_have_content(&header_cells) {
|
||||
return false;
|
||||
}
|
||||
let Some(alignments) = parse_alignments(&split_table_cells(&align_line)) else {
|
||||
return false;
|
||||
};
|
||||
if header_cells.len() != alignments.len() {
|
||||
return false;
|
||||
}
|
||||
let body_line = trim_table_line(&lines[index + 2]);
|
||||
body_line.text.contains('|') && !is_table_block_break(&body_line.text)
|
||||
}
|
||||
|
||||
pub(crate) fn is_heading_start(line: &str, flags: u32) -> bool {
|
||||
if !ParserFlags::has(flags, ParserFlags::ALLOW_HEADINGS) || !starts_with(line, "#") {
|
||||
return false;
|
||||
|
||||
@@ -159,6 +159,11 @@ impl RuntimeState<'_> {
|
||||
let next_line = &self.lines[self.current_line + consumed].text;
|
||||
let trimmed_next = trim_start(next_line);
|
||||
if crate::block::is_block_start(trimmed_next, self.parser.flags())
|
||||
|| crate::block::is_table_start(
|
||||
&self.lines,
|
||||
self.current_line + consumed,
|
||||
self.parser.flags(),
|
||||
)
|
||||
|| trimmed_next.is_empty()
|
||||
{
|
||||
break;
|
||||
|
||||
@@ -404,3 +404,26 @@ proptest::proptest! {
|
||||
prop_assert!(serialized.starts_with('['));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_parser_starts_a_table_directly_after_a_text_line() {
|
||||
let input =
|
||||
"intro text\r\n| Framework | Type |\r\n| --------- | ---- |\r\n| React | UI library |";
|
||||
let parsed = parse(input, ParserFlags::ALL, "");
|
||||
let nodes = parsed["nodes"].as_array().expect("nodes array");
|
||||
assert!(
|
||||
nodes.iter().any(|node| node["type"] == "Table"),
|
||||
"expected a Table node, got {parsed}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_parser_still_requires_a_delimiter_row_for_a_table() {
|
||||
let input = "intro text\n| not | a | table |\njust more text";
|
||||
let parsed = parse(input, ParserFlags::ALL, "");
|
||||
let nodes = parsed["nodes"].as_array().expect("nodes array");
|
||||
assert!(
|
||||
!nodes.iter().any(|node| node["type"] == "Table"),
|
||||
"pipes alone must not form a table, got {parsed}"
|
||||
);
|
||||
}
|
||||
Generated
+5
-5
@@ -1674,8 +1674,8 @@ importers:
|
||||
specifier: 7.0.0-dev.20260130.1
|
||||
version: 7.0.0-dev.20260130.1
|
||||
electron:
|
||||
specifier: 43.4.1
|
||||
version: 43.4.1
|
||||
specifier: 43.4.0
|
||||
version: 43.4.0
|
||||
electron-builder:
|
||||
specifier: 26.9.0
|
||||
version: 26.9.0([email protected])
|
||||
@@ -8216,8 +8216,8 @@ packages:
|
||||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-5b+EuiwkgG5iRcsEL34rimgRpkYp15SsfZOa0pC5kXs0Tb82TH4n95rpQzTZa7yRCbA7tm0WoEbuBL6NaAhAcA==}
|
||||
[email protected].0:
|
||||
resolution: {integrity: sha512-3qxGF0CeQbiox5oWV1JlbWGQ1VerbmDhTFqW4sJ8h7uqTHniFYPObXJcDna0DMh32et0fFyKzz0YY8lJv3t5jg==}
|
||||
engines: {node: '>= 22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -18308,7 +18308,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected].1:
|
||||
[email protected].0:
|
||||
dependencies:
|
||||
'@electron-internal/extract-zip': 1.0.5
|
||||
'@electron/get': 5.1.0
|
||||
|
||||
Reference in new issue
Block a user