Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
66 changes: 60 additions & 6 deletions apps/native-component-list/src/screens/ScreenCaptureScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as ScreenCapture from 'expo-screen-capture';
import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { FlatList, Platform, StyleSheet, Text, View } from 'react-native';

import HeadingText from '../components/HeadingText';
import MonoText from '../components/MonoText';
Expand Down Expand Up @@ -33,6 +33,7 @@ function useScreenCapture(onCapture: () => void) {

export default function ScreenCaptureScreen() {
const [isEnabled, setEnabled] = React.useState(true);
const [isAppSwitcherProtectionEnabled, setAppSwitcherProtectionEnabled] = React.useState(false);
const [timestamps, setTimestamps] = React.useState<Date[]>([]);

React.useEffect(() => {
Expand All @@ -43,10 +44,23 @@ export default function ScreenCaptureScreen() {
}
}, [isEnabled]);

// We need to allow screen capture when the component unmounts to allow screen capture again on Android/iOS
React.useEffect(() => {
if (Platform.OS === 'ios') {
if (isAppSwitcherProtectionEnabled) {
ScreenCapture.enableAppSwitcherProtectionAsync();
} else {
ScreenCapture.disableAppSwitcherProtectionAsync();
}
}
}, [isAppSwitcherProtectionEnabled]);

// Clean up on unmount: allow screen capture on all platforms, disable app switcher protection on iOS
React.useEffect(() => {
return () => {
ScreenCapture.allowScreenCaptureAsync();
if (Platform.OS === 'ios') {
ScreenCapture.disableAppSwitcherProtectionAsync();
}
};
}, []);

Expand All @@ -55,15 +69,34 @@ export default function ScreenCaptureScreen() {
return (
<View style={styles.container}>
<TitleSwitch title="Screen Capture Allowed" value={isEnabled} setValue={setEnabled} />
<Text style={{ padding: 8 }}>
<Text style={styles.description}>
Take a screenshot or attempt to record the screen to test that the image is/isn't obscured.
</Text>
<HeadingText>Capture Timestamps</HeadingText>
<Text>Take a screenshot to test if the listener works.</Text>

{Platform.OS === 'ios' && (
<>
<TitleSwitch
title="App Switcher Protection"
value={isAppSwitcherProtectionEnabled}
setValue={setAppSwitcherProtectionEnabled}
style={styles.switchSpacing}
/>
<Text style={styles.description}>
When enabled, shows blur overlay when app is not in focus.{'\n'}
Test by opening app switcher or going to background.
</Text>
</>
)}

<HeadingText style={styles.heading}>Screenshot Timestamps</HeadingText>
<Text style={styles.timestampDescription}>
Take a screenshot to test if the listener works.
</Text>
<FlatList
data={timestamps}
keyExtractor={(item) => item.getTime() + '-'}
renderItem={({ item }) => <MonoText>{item.toLocaleTimeString()}</MonoText>}
style={styles.timestampList}
/>
</View>
);
Expand All @@ -72,7 +105,28 @@ export default function ScreenCaptureScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
alignItems: 'center',
justifyContent: 'center',
},
description: {
padding: 8,
textAlign: 'center',
marginBottom: 16,
},
switchSpacing: {
marginTop: 16,
},
heading: {
marginTop: 24,
marginBottom: 8,
},
timestampDescription: {
textAlign: 'center',
paddingHorizontal: 16,
marginBottom: 16,
},
timestampList: {
maxHeight: 200,
width: '100%',
},
});
8 changes: 8 additions & 0 deletions apps/router-e2e/__e2e__/compiler/app/hooks/useBananas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FruitLabelPrefix } from './useFruit';

export function useBananas() {
if (!FruitLabelPrefix) {
throw new Error('Prefix is not defined.');
}
console.log(`${FruitLabelPrefix} Bananas are great!`);
}
9 changes: 9 additions & 0 deletions apps/router-e2e/__e2e__/compiler/app/hooks/useFruit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useBananas } from './useBananas';

export function useFruit() {
console.log(`${FruitLabelPrefix} Fruits are delicious!`);
}

export const FruitLabelPrefix = 'Fresh';

export { useBananas };
3 changes: 3 additions & 0 deletions apps/router-e2e/__e2e__/compiler/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useEffect, useState } from 'react';
import { Text } from 'react-native';

import { useBananas } from './hooks/useFruit';

export default function Page() {
useBananas();
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/more/expo-cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ From here, you can choose to generate basic project files like:
| `EXPO_NO_REACT_NATIVE_WEB` | **boolean** | <div className="flex items-center pb-1.5"><StatusTag status="experimental" /><StatusTag status="SDK 52+" /></div>Enable experimental mode where React Native Web isn't required to run Expo apps on web. |
| `EXPO_NO_DEPENDENCY_VALIDATION` | **boolean** | <div className="flex items-center pb-1.5"><StatusTag status="SDK 52+" /></div>Disable built-in dependency validation when installing packages through `npx expo install` and `npx expo start`. |
| `EXPO_WEB_DEV_HYDRATE` | **boolean** | Enable React hydration in development for a web project. This can help you identify hydration issues early. |
| `EXPO_UNSTABLE_LIVE_BINDINGS` | **boolean** | <div className="flex items-center pb-1.5"><StatusTag status="experimental" /><StatusTag status="SDK 55+" /></div>Disable live binding in experimental import export support. Enabled by default. Live bindings improve circular dependencies support, but can lead to slightly worse performance. |

## Telemetry

Expand Down
1 change: 1 addition & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Support JSON output for install check. ([#37318](https://github.com/expo/expo/pull/37318) by [@betomoedano](https://github.com/betomoedano))
- Add `EXPO_USE_STICKY_RESOLVER` to enable experimental sticky resolution to native modules ([#37201](https://github.com/expo/expo/pull/37201) by [@kitten](https://github.com/kitten))
- Support external URLs with static redirects ([#38041](https://github.com/expo/expo/pull/38041) by [@hassankhan](https://github.com/hassankhan))
- Add `EXPO_UNSTABLE_LIVE_BINDINGS` to allow developer to disable live binding in `experimentalImportSupport`. ([#38135](https://github.com/expo/expo/pull/38135) by [@krystofwoldrich](https://github.com/krystofwoldrich))

### 🐛 Bug fixes

Expand Down
160 changes: 127 additions & 33 deletions packages/@expo/cli/e2e/playwright/dev/react-compiler.test.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,153 @@
import { expect, test } from '@playwright/test';
import klawSync from 'klaw-sync';
import fs from 'node:fs';
import path from 'node:path';

import { clearEnv, restoreEnv } from '../../__tests__/export/export-side-effects';
import { getRouterE2ERoot } from '../../__tests__/utils';
import { createExpoServe, executeExpoAsync } from '../../utils/expo';
import { pageCollectErrors } from '../page';
import { assert } from 'node:console';

test.beforeAll(() => clearEnv());
test.afterAll(() => restoreEnv());

const projectRoot = getRouterE2ERoot();
const inputDir = 'dist-react-compiler';
const baseDir = 'dist-react-compiler';

test.describe(inputDir, () => {
test.describe(baseDir, () => {
const expoServe = createExpoServe({
cwd: projectRoot,
env: {
NODE_ENV: 'production',
},
});

test.beforeEach('bundle and serve', async () => {
console.time('expo export');
await executeExpoAsync(projectRoot, ['export', '-p', 'web', '--output-dir', inputDir], {
env: {
NODE_ENV: 'production',
EXPO_USE_STATIC: 'static',
E2E_ROUTER_SRC: 'compiler',
E2E_ROUTER_COMPILER: 'true',
},
});
console.timeEnd('expo export');

console.time('npx serve');
await expoServe.startAsync([inputDir]);
console.timeEnd('npx serve');
});
test.afterEach(async () => {
await expoServe.stopAsync();
test.describe('default', () => {
const inputDir = 'dist-react-compiler-default';

test.beforeEach('bundle and serve', async () => {
console.time('expo export');
await executeExpoAsync(projectRoot, ['export', '-p', 'web', '--output-dir', inputDir], {
env: {
NODE_ENV: 'production',
EXPO_USE_STATIC: 'static',
E2E_ROUTER_SRC: 'compiler',
E2E_ROUTER_COMPILER: 'true',
},
});
console.timeEnd('expo export');

console.time('npx serve');
await expoServe.startAsync([inputDir]);
console.timeEnd('npx serve');
});
test.afterEach(async () => {
await expoServe.stopAsync();
});

test('bundle contains live bindings', async () => {
const jsFiles = klawSync(path.join(projectRoot, inputDir, '_expo/static/js'), {
nodir: true,
});
const bundleFile = jsFiles[0]?.path;

// Sanity check
assert(jsFiles.length === 1, 'This test expects a single JS bundle file to be generated.');
assert(bundleFile, 'No JS bundle file found.');

const bundleContent = fs.readFileSync(bundleFile, 'utf8');

// The useBananas code which otherwise causes the app to crash uses live bindings.
expect(bundleContent).toContain('Object.defineProperty(e,"useBananas",{enumerable:!0,get:function(){return n.useBananas}})}');
});

// This test generally ensures no errors are thrown during an export loading.
test('loads compiler', async ({ page }) => {
// Listen for console logs and errors
const pageErrors = pageCollectErrors(page);

console.time('Open page');
// Navigate to the app
await page.goto(expoServe.url.href);
console.timeEnd('Open page');

console.time('hydrate');
// Wait for the app to load
await expect(page.locator('[data-testid="react-compiler"]')).toHaveText('2');
console.timeEnd('hydrate');

expect(pageErrors.all).toEqual([]);
});
});

// This test generally ensures no errors are thrown during an export loading.
test('loads compiler', async ({ page }) => {
// Listen for console logs and errors
const pageErrors = pageCollectErrors(page);
test.describe('without live bindings', () => {
const inputDir = 'dist-react-compiler-no-live-bindings';

test.beforeEach('bundle and serve', async () => {
console.time('expo export');
const res = await executeExpoAsync(
projectRoot,
['export', '-p', 'web', '--output-dir', inputDir],
{
env: {
NODE_ENV: 'production',
EXPO_USE_STATIC: 'static',
E2E_ROUTER_SRC: 'compiler',
E2E_ROUTER_COMPILER: 'true',
EXPO_UNSTABLE_LIVE_BINDINGS: 'false',
},
}
);
console.timeEnd('expo export');

console.log('expo export result:', res.stdout, res.stderr);

console.time('npx serve');
await expoServe.startAsync([inputDir]);
console.timeEnd('npx serve');
});
test.afterEach(async () => {
await expoServe.stopAsync();
});

test('bundle does not have live bindings', async () => {
const jsFiles = klawSync(path.join(projectRoot, inputDir, '_expo/static/js'), {
nodir: true,
});
const bundleFile = jsFiles[0]?.path;

console.time('Open page');
// Navigate to the app
await page.goto(expoServe.url.href);
console.timeEnd('Open page');
// Sanity check
assert(jsFiles.length === 1, 'This test expects a single JS bundle file to be generated.');
assert(bundleFile, 'No JS bundle file found.');

console.time('hydrate');
// Wait for the app to load
await expect(page.locator('[data-testid="react-compiler"]')).toHaveText('2');
console.timeEnd('hydrate');
const bundleContent = fs.readFileSync(bundleFile, 'utf8');

expect(pageErrors.all).toEqual([]);
// The useBananas code which causes the application to crash uses static bindings.
expect(bundleContent).not.toContain('Object.defineProperty(e,"useBananas",{enumerable:!0,get:function(){return n.useBananas}})}');
expect(bundleContent).toContain('e.useBananas=function()');
});

// This test generally ensures no errors are thrown during an export loading.
test('fails to load', async ({ page }) => {
// Listen for console logs and errors
const pageErrors = pageCollectErrors(page);

console.time('Open page');
// Navigate to the app
await page.goto(expoServe.url.href);
console.timeEnd('Open page');

// Check for errors up to 4 times
for (let i = 0; i < 4; i++) {
if (pageErrors.errors.length > 0) {
break;
}
console.log(`Waiting for errors, attempt ${i + 1}/4`);
await page.waitForTimeout(500);
}

expect(pageErrors.errors).toContainEqual(new Error('Prefix is not defined.'));
});
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { env } from 'node:process';

import { createBundleUrlPath, getMetroDirectBundleOptions } from '../metroOptions';

describe(getMetroDirectBundleOptions, () => {
Expand Down Expand Up @@ -28,7 +30,9 @@ describe(getMetroDirectBundleOptions, () => {
})
).toEqual({
customResolverOptions: {},
customTransformOptions: { baseUrl: '/foo/' },
customTransformOptions: {
baseUrl: '/foo/',
},
serializerOptions: {},
dev: true,
entryFile: '/index.js',
Expand Down Expand Up @@ -67,6 +71,22 @@ describe(getMetroDirectBundleOptions, () => {
unstable_transformProfile: 'default',
});
});
describe(`live bindings`, () => {
afterEach(() => {
delete env.EXPO_UNSTABLE_LIVE_BINDINGS;
});
it(`enables live bindings by default`, () => {
expect(getMetroDirectBundleOptions({}).customTransformOptions?.liveBindings).toBeUndefined();
});
it(`enables live bindings by default`, () => {
env.EXPO_UNSTABLE_LIVE_BINDINGS = 'true';
expect(getMetroDirectBundleOptions({}).customTransformOptions?.liveBindings).toBeUndefined();
});
it(`enables live bindings by default`, () => {
env.EXPO_UNSTABLE_LIVE_BINDINGS = '0';
expect(getMetroDirectBundleOptions({}).customTransformOptions?.liveBindings).toBe('false');
});
});
});
describe(createBundleUrlPath, () => {
it(`returns basic options`, () => {
Expand Down
Loading
Loading