Skip to content
Open
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
154 changes: 154 additions & 0 deletions packages/core/ui/frame/activity-callbacks.android.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Application } from '../../application/application';
import { ActivityCallbacksImplementation } from './index.android';

vi.mock('./fragment.transitions', () => ({}));
vi.mock('./fragment', () => ({}));
vi.mock('../embedding', () => ({ isEmbedded: () => false }));

function createActivity() {
return { getIntent: () => null, setContentView: () => {} };
}

function createRootView(context: object, isLoaded = true) {
const order: string[] = [];
const view = {
_context: context,
_domId: 1,
isLoaded,
nativeViewProtected: {},
callUnloaded() {
order.push('unloaded');
view.isLoaded = false;
},
callLoaded() {
order.push('loaded');
view.isLoaded = true;
},
_setupAsRootView(activity: object) {
order.push('setup');
view._context = activity;
},
_tearDownUI: vi.fn(),
};

return { view, order };
}

function createCallbacks(rootView?: object) {
const callbacks = new ActivityCallbacksImplementation();
(callbacks as any)._rootView = rootView;

return callbacks;
}

describe('activity callbacks with a root view shared between activities', () => {
const superFunc = vi.fn();
let previousAndroid: PropertyDescriptor | undefined;

beforeEach(() => {
previousAndroid = Object.getOwnPropertyDescriptor(Application, 'android');
superFunc.mockClear();
vi.stubGlobal('org', { nativescript: { widgets: { CommonLayoutParams: class {} } } });
vi.spyOn(Application, 'createRootView').mockImplementation((view) => view);
vi.spyOn(Application, 'initRootView').mockImplementation(() => {});
Object.defineProperty(Application, 'android', {
configurable: true,
value: { _getWindowForActivity: () => undefined },
});
});

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
if (previousAndroid) {
Object.defineProperty(Application, 'android', previousAndroid);
} else {
delete (Application as any).android;
}
});

describe('an activity that no longer shows the root', () => {
const stale = createActivity();
const current = createActivity();

it('keeps the root loaded when it stops', () => {
const { view, order } = createRootView(current);

createCallbacks(view).onStop(stale, superFunc);

expect(order).toEqual([]);
expect(superFunc).toHaveBeenCalledTimes(1);
});

it('keeps the root intact when it is destroyed', () => {
const { view } = createRootView(current);

createCallbacks(view).onDestroy(stale, superFunc);

expect(view._tearDownUI).not.toHaveBeenCalled();
expect(superFunc).toHaveBeenCalledTimes(1);
});

it('does not save the root id for a later restore', () => {
const { view } = createRootView(current);
const outState = { putInt: vi.fn() };

createCallbacks(view).onSaveInstanceState(stale as any, outState as any, superFunc);

expect(outState.putInt).not.toHaveBeenCalled();
expect(superFunc).toHaveBeenCalledTimes(1);
});

it('takes the root back when it starts again', () => {
const { view, order } = createRootView(current);

createCallbacks(view).onStart(stale, superFunc);

expect(order).toEqual(['unloaded', 'setup', 'loaded']);
expect(view._context).toBe(stale);
});
});

describe('the activity showing the root', () => {
const showing = createActivity();

it('unloads the root when it stops', () => {
const { view, order } = createRootView(showing);

createCallbacks(view).onStop(showing, superFunc);

expect(order).toEqual(['unloaded']);
});

it('tears down the root when it is destroyed', () => {
const { view } = createRootView(showing);

createCallbacks(view).onDestroy(showing, superFunc);

expect(view._tearDownUI).toHaveBeenCalledWith(true);
});

it('loads the root without setting it up again when it starts', () => {
const { view, order } = createRootView(showing, false);

createCallbacks(view).onStart(showing, superFunc);

expect(order).toEqual(['loaded']);
});
});

it('keeps the root in a new activity that adopts it before the old one stops and is destroyed', () => {
const old = createActivity();
const next = createActivity();
const { view, order } = createRootView(old);

createCallbacks().resetActivityContent(next as any, view as any);
createCallbacks(view).onStop(old, superFunc);
createCallbacks(view).onDestroy(old, superFunc);

expect(order).toEqual(['unloaded', 'setup', 'loaded']);
expect(view._tearDownUI).not.toHaveBeenCalled();
expect(view._context).toBe(next);
});
});
18 changes: 15 additions & 3 deletions packages/core/ui/frame/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
@profile
public onSaveInstanceState(activity: androidx.appcompat.app.AppCompatActivity, outState: android.os.Bundle, superFunc: Function): void {
superFunc.call(activity, outState);
const rootView = this._rootView;
const rootView = this.getRootViewShownBy(activity);
if (rootView instanceof Frame) {
outState.putInt(INTENT_EXTRA, rootView.android.frameId);
rootView._saveFragmentsState();
Expand Down Expand Up @@ -926,6 +926,10 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
}

const rootView = this._rootView;
if (rootView && rootView._context !== activity && !isEmbedded()) {
this.setActivityContent(activity, null, false);
}

if (rootView && !rootView.isLoaded && !isEmbedded()) {
rootView.callLoaded();
}
Expand All @@ -939,7 +943,7 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
Trace.write('NativeScriptActivity.onStop();', Trace.categories.NativeLifecycle);
}

const rootView = this._rootView;
const rootView = this.getRootViewShownBy(activity);
if (rootView && rootView.isLoaded && !isEmbedded()) {
rootView.callUnloaded();
}
Expand Down Expand Up @@ -970,7 +974,7 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
Trace.write('NativeScriptActivity.onDestroy();', Trace.categories.NativeLifecycle);
}

const rootView = this._rootView;
const rootView = this.getRootViewShownBy(activity);
if (rootView) {
rootView._tearDownUI(true);
}
Expand Down Expand Up @@ -1088,6 +1092,10 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
Application.android.notify(resultArgs);
}

private getRootViewShownBy(activity: any): View | undefined {
return this._rootView?._context === activity ? this._rootView : undefined;
}

public resetActivityContent(activity: androidx.appcompat.app.AppCompatActivity, view?: View): void {
if (this._rootView) {
const manager = this._rootView._getFragmentManager();
Expand Down Expand Up @@ -1160,6 +1168,10 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks

activityRootViewsMap.set(rootView._domId, new WeakRef(rootView));

if (rootView.isLoaded && rootView._context !== activity && !isEmbedded()) {
rootView.callUnloaded();
}

// setup view as styleScopeHost
rootView._setupAsRootView(activity);

Expand Down
Loading