-
Notifications
You must be signed in to change notification settings - Fork 1
JS-driven screenshot approach via ScreenshotHelper native module #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
EmilioBejasa
wants to merge
2
commits into
main
Choose a base branch
from
ui-thread-approach
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
android/app/src/androidTest/java/com/testapp/ComponentSurfaceTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.rnstorybookautoscreenshots | ||
|
|
||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| class ComponentSurfaceTest { | ||
|
|
||
| @Test | ||
| fun screenshotAll() { | ||
| ReactHostFixture.screenshotAll(listOf( | ||
| // Button story variants | ||
| "Button_Primary", | ||
| "Button_Secondary", | ||
| "Button_Large", | ||
| "Button_Small", | ||
|
|
||
| // Header story variants | ||
| "Header_LoggedIn", | ||
| "Header_LoggedOut", | ||
|
|
||
| // MyFeature story variants | ||
| "MyFeature_Initial", | ||
| "MyFeature_WithClicks", | ||
| "MyFeature_ManyClicks", | ||
|
|
||
| // SwitchFeature story variants | ||
| "SwitchFeature_Off", | ||
| "SwitchFeature_On", | ||
|
|
||
| // TimerFeature story variants | ||
| "TimerFeature_Initial", | ||
| "TimerFeature_Running", | ||
| "TimerFeature_Paused", | ||
| "TimerFeature_LongDuration", | ||
|
|
||
| // CoinFlipFeature story variants | ||
| "CoinFlipFeature_Default", | ||
| "CoinFlipFeature_Heads", | ||
| "CoinFlipFeature_Tails", | ||
| "CoinFlipFeature_ManyFlips", | ||
| )) | ||
| } | ||
| } |
43 changes: 1 addition & 42 deletions
43
android/app/src/androidTest/java/com/testapp/IsolatedTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,55 +1,14 @@ | ||
| package com.rnstorybookautoscreenshots | ||
|
|
||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import com.testapp.MainApplication | ||
| import junit.framework.TestCase.assertNotNull | ||
| import junit.framework.TestCase.assertTrue | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
| import com.facebook.testing.screenshot.ViewHelpers | ||
| import com.facebook.testing.screenshot.Screenshot | ||
| import org.junit.Assert.*; | ||
| import com.facebook.react.interfaces.* | ||
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| class IsolatedTest { | ||
| @Test | ||
| fun simpleTest() { | ||
| assertTrue(true) | ||
| } | ||
|
|
||
| @Test | ||
| fun constructViewTest() { | ||
| val context = InstrumentationRegistry.getInstrumentation().targetContext | ||
| val app = context.applicationContext as MainApplication | ||
| val surface = app.reactHost.createSurface(context, "SimpleTestComponent", null) | ||
| assertEquals("SimpleTestComponent", surface.moduleName) | ||
|
|
||
| // TODO: we aren't 100% sure if prerender() and start() are being called the way we want it to. | ||
| // We probably want to create a ReactHost directly instead of taking it from the MainApplication... probably | ||
| // Also look up bridge-less mode. | ||
| assertGoodTask(surface.prerender()) | ||
|
|
||
|
|
||
| assertNotNull(surface.view) | ||
|
|
||
| ViewHelpers.setupView(surface.view!!) | ||
| .setExactHeightPx(1000) | ||
| .setExactWidthPx(1000) | ||
| .layout() | ||
|
|
||
|
|
||
| val ti = surface.start() | ||
| assertGoodTask(ti) | ||
|
|
||
| Screenshot.snap(surface.view!!) | ||
| .record() | ||
| ReactHostFixture.screenshotComponent("ConstructViewTest") | ||
| } | ||
| } | ||
|
|
||
| fun assertGoodTask(ti : TaskInterface<Void>) { | ||
| ti.waitForCompletion() | ||
| assertFalse(ti.isFaulted()) | ||
| assertTrue(ti.isCompleted()) | ||
| } |
106 changes: 106 additions & 0 deletions
106
android/app/src/androidTest/java/com/testapp/ReactHostFixture.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package com.rnstorybookautoscreenshots | ||
|
|
||
| import android.app.Application | ||
| import android.graphics.Color | ||
| import android.view.ContextThemeWrapper | ||
| import android.view.View | ||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import com.facebook.react.PackageList | ||
| import com.facebook.react.bridge.JSBundleLoader | ||
| import com.facebook.react.common.annotations.UnstableReactNativeAPI | ||
| import com.facebook.react.defaults.DefaultComponentsRegistry | ||
| import com.facebook.react.defaults.DefaultReactHostDelegate | ||
| import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate | ||
| import com.facebook.react.fabric.ComponentFactory | ||
| import com.facebook.react.interfaces.fabric.ReactSurface | ||
| import com.facebook.react.runtime.ReactHostImpl | ||
| import com.facebook.react.runtime.hermes.HermesInstance | ||
| import com.facebook.testing.screenshot.ViewHelpers | ||
| import com.facebook.testing.screenshot.WindowAttachment | ||
| import com.testapp.R | ||
| import com.testapp.ScreenshotHelperModule | ||
| import com.testapp.ScreenshotHelperPackage | ||
| import junit.framework.TestCase.assertNotNull | ||
| import org.junit.Assert.assertTrue | ||
| import java.lang.ref.WeakReference | ||
| import java.util.concurrent.CountDownLatch | ||
| import java.util.concurrent.TimeUnit | ||
|
|
||
| @OptIn(UnstableReactNativeAPI::class) | ||
| object ReactHostFixture { | ||
| val reactHost: ReactHostImpl by lazy { | ||
| val instrumentation = InstrumentationRegistry.getInstrumentation() | ||
| val context = instrumentation.targetContext | ||
| val app = context.applicationContext as Application | ||
|
|
||
| val bundleLoader = JSBundleLoader.createAssetLoader( | ||
| context, "assets://index.android.bundle", true) | ||
| val delegate = DefaultReactHostDelegate( | ||
| jsMainModulePath = "index", | ||
| jsBundleLoader = bundleLoader, | ||
| reactPackages = PackageList(app).packages + listOf(ScreenshotHelperPackage()), | ||
| jsRuntimeFactory = HermesInstance(), | ||
| turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(), | ||
| exceptionHandler = { throw it }, | ||
| ) | ||
| val componentFactory = ComponentFactory() | ||
| DefaultComponentsRegistry.register(componentFactory) | ||
| val host = ReactHostImpl(context, delegate, componentFactory, false, false) | ||
| instrumentation.runOnMainSync { host.onHostResume(null) } | ||
| host | ||
| } | ||
|
|
||
| /** | ||
| * Starts all [componentNames] surfaces simultaneously, waits for each JS component | ||
| * to call ScreenshotHelper.takeScreenshot, then tears everything down. | ||
| * | ||
| * All surfaces are attached and started in a single runOnMainSync, so the JS thread | ||
| * queues work for all of them at once instead of waiting for each test to finish | ||
| * before the next begins. | ||
| */ | ||
| @OptIn(UnstableReactNativeAPI::class) | ||
| fun screenshotAll(componentNames: List<String>) { | ||
| val instrumentation = InstrumentationRegistry.getInstrumentation() | ||
| val latch = CountDownLatch(componentNames.size) | ||
| ScreenshotHelperModule.sharedLatch = latch | ||
|
|
||
| val surfaces = mutableListOf<ReactSurface>() | ||
| val detachers = mutableListOf<WindowAttachment.Detacher?>() | ||
|
|
||
| componentNames.forEach { name -> | ||
| val context = ContextThemeWrapper(instrumentation.targetContext, R.style.AppTheme) | ||
| val surface = reactHost.createSurface(context, name, null) | ||
| assertNotNull("No view for $name", surface.view) | ||
| ScreenshotHelperModule.pendingViews[name] = WeakReference(surface.view!!) | ||
| surfaces.add(surface) | ||
| detachers.add(null) | ||
| } | ||
|
|
||
| try { | ||
| instrumentation.runOnMainSync { | ||
| surfaces.forEachIndexed { i, surface -> | ||
| val view = surface.view!! | ||
| view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) | ||
| view.setBackgroundColor(Color.WHITE) | ||
| detachers[i] = WindowAttachment.dispatchAttach(view) | ||
| ViewHelpers.setupView(view).setExactWidthPx(1080).setExactHeightPx(1920).layout() | ||
| surface.start() | ||
| } | ||
| } | ||
|
|
||
| assertTrue( | ||
| "Timed out waiting for all screenshots: $componentNames", | ||
| latch.await(60, TimeUnit.SECONDS) | ||
| ) | ||
| } finally { | ||
| ScreenshotHelperModule.pendingViews.clear() | ||
| ScreenshotHelperModule.sharedLatch = null | ||
| instrumentation.runOnMainSync { | ||
| surfaces.forEach { it.stop() } | ||
| detachers.forEach { it?.detach() } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fun screenshotComponent(componentName: String) = screenshotAll(listOf(componentName)) | ||
| } | ||
43 changes: 43 additions & 0 deletions
43
android/app/src/androidTest/java/com/testapp/ScreenshotHelperModule.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.testapp | ||
|
|
||
| import android.view.View | ||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.bridge.ReactContextBaseJavaModule | ||
| import com.facebook.react.bridge.ReactMethod | ||
| import com.facebook.testing.screenshot.Screenshot | ||
| import java.lang.ref.WeakReference | ||
| import java.util.concurrent.ConcurrentHashMap | ||
| import java.util.concurrent.CountDownLatch | ||
|
|
||
| /** | ||
| * Native module called from JS when a component has finished rendering. | ||
| * | ||
| * [pendingViews] maps component name → view to screenshot. | ||
| * [sharedLatch] is counted down once per component that calls back, | ||
| * so the test thread can await all of them concurrently. | ||
| */ | ||
| class ScreenshotHelperModule(reactContext: ReactApplicationContext) | ||
| : ReactContextBaseJavaModule(reactContext) { | ||
|
|
||
| companion object { | ||
| val pendingViews = ConcurrentHashMap<String, WeakReference<View>>() | ||
| var sharedLatch: CountDownLatch? = null | ||
| } | ||
|
|
||
| override fun getName() = "ScreenshotHelper" | ||
|
|
||
| @ReactMethod | ||
| fun takeScreenshot(name: String) { | ||
| val view = pendingViews[name]?.get() | ||
| if (view == null) { | ||
| sharedLatch?.countDown() | ||
| return | ||
| } | ||
| val instrumentation = InstrumentationRegistry.getInstrumentation() | ||
| instrumentation.runOnMainSync { | ||
| Screenshot.snap(view).setName(name).record() | ||
| } | ||
| sharedLatch?.countDown() | ||
| } | ||
| } |
14 changes: 14 additions & 0 deletions
14
android/app/src/androidTest/java/com/testapp/ScreenshotHelperPackage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.testapp | ||
|
|
||
| import com.facebook.react.ReactPackage | ||
| import com.facebook.react.bridge.NativeModule | ||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.uimanager.ViewManager | ||
|
|
||
| class ScreenshotHelperPackage : ReactPackage { | ||
| override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = | ||
| listOf(ScreenshotHelperModule(reactContext)) | ||
|
|
||
| override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = | ||
| emptyList() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after PR #103 is approved and merged, I want you to refactor the code from IsolatedTest, not copy-paste a completely duplicate version