chromium/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/TabStateAttributesTest.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.tab;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import android.os.Handler;
import android.os.Looper;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.ObserverList.RewindableIterator;
import org.chromium.base.Token;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.task.test.ShadowPostTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

import java.util.concurrent.TimeUnit;

/** Unit tests for TabStateAttributes. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {ShadowLooper.class, ShadowPostTask.class})
public class TabStateAttributesTest {
    @Rule public final MockitoRule mockito = MockitoJUnit.rule();

    @Mock private Profile mProfile;
    @Mock private WebContents mWebContents;
    @Mock private TabStateAttributes.Observer mAttributesObserver;

    @Captor ArgumentCaptor<WebContentsObserver> mWebContentsObserverCaptor;

    private MockTab mTab;

    @Before
    public void setUp() {
        ShadowPostTask.setTestImpl(
                (@TaskTraits int taskTraits, Runnable task, long delay) -> {
                    new Handler(Looper.getMainLooper()).postDelayed(task, delay);
                });

        mTab =
                new MockTab(0, mProfile) {
                    @Override
                    public WebContents getWebContents() {
                        return mWebContents;
                    }

                    @Override
                    public boolean isInitialized() {
                        return true;
                    }
                };
        mTab.setCanGoForward(false);
        mTab.setCanGoBack(false);

        doNothing().when(mWebContents).addObserver(mWebContentsObserverCaptor.capture());
    }

    @Test
    public void testDefaultDirtyState() {
        TabStateAttributes.createForTab(mTab, null);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        mTab.getUserDataHost().removeUserData(TabStateAttributes.class);

        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        mTab.getUserDataHost().removeUserData(TabStateAttributes.class);

        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_FOR_LAZY_LOAD);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        mTab.getUserDataHost().removeUserData(TabStateAttributes.class);

        TabStateAttributes.createForTab(mTab, TabCreationState.LIVE_IN_BACKGROUND);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        mTab.getUserDataHost().removeUserData(TabStateAttributes.class);

        TabStateAttributes.createForTab(mTab, TabCreationState.LIVE_IN_FOREGROUND);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        mTab.getUserDataHost().removeUserData(TabStateAttributes.class);
    }

    @Test
    public void testTitleUpdate() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onTitleUpdated(mTab);

        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.UNTIDY);
    }

    @Test
    public void testFinishMainFrameNavigation() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onContentChanged(mTab);
        WebContentsObserver webContentsObserver = mWebContentsObserverCaptor.getValue();
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);
        GURL testGURL = JUnitTestGURLs.EXAMPLE_URL;
        NavigationHandle navHandle = NavigationHandle.createForTesting(testGURL, false, 0, false);

        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        webContentsObserver.didFinishNavigationInPrimaryMainFrame(navHandle);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.UNTIDY);
    }

    @Test
    public void testPageLoadFinished() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);
        GURL testGURL = JUnitTestGURLs.EXAMPLE_URL;

        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        while (observers.hasNext()) observers.next().onPageLoadFinished(mTab, testGURL);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.UNTIDY);
    }

    @Test
    public void testLoadStopped_DifferentDocument() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);

        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        while (observers.hasNext()) {
            observers.next().onLoadStopped(mTab, /* toDifferentDocument= */ true);
        }
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).setStateForTesting(TabStateAttributes.DirtinessState.UNTIDY);
        observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) {
            observers.next().onLoadStopped(mTab, /* toDifferentDocument= */ true);
        }
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
    }

    @Test
    public void testLoadStopped_SameDocument() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);

        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        while (observers.hasNext()) {
            observers.next().onLoadStopped(mTab, /* toDifferentDocument= */ false);
        }
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);

        ShadowLooper.idleMainLooper();
        TabStateAttributes.from(mTab).setStateForTesting(TabStateAttributes.DirtinessState.UNTIDY);
        observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) {
            observers.next().onLoadStopped(mTab, /* toDifferentDocument= */ false);
        }
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        assertEquals(1, Robolectric.getForegroundThreadScheduler().size());

        // An additional call to onLoadStopped should not change the state, nor should another
        // task be queued.
        observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) {
            observers.next().onLoadStopped(mTab, /* toDifferentDocument= */ false);
        }
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        assertEquals(1, Robolectric.getForegroundThreadScheduler().size());

        Robolectric.getForegroundThreadScheduler()
                .advanceBy(
                        TabStateAttributes.DEFAULT_LOW_PRIORITY_SAVE_DELAY_MS,
                        TimeUnit.MILLISECONDS);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        assertEquals(0, Robolectric.getForegroundThreadScheduler().size());
    }

    @Test
    public void testHide() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);

        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onHidden(mTab, TabHidingType.CHANGED_TABS);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);

        // If a tab is not closing, then hiding the tab should mark it as dirty.
        TabStateAttributes.from(mTab).setStateForTesting(TabStateAttributes.DirtinessState.UNTIDY);
        mTab.setClosing(false);
        observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onHidden(mTab, TabHidingType.CHANGED_TABS);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);

        // If a tab is closing, then hiding the tab should not mark it as dirty.
        TabStateAttributes.from(mTab).setStateForTesting(TabStateAttributes.DirtinessState.CLEAN);
        mTab.setClosing(true);
        while (observers.hasNext()) observers.next().onHidden(mTab, TabHidingType.CHANGED_TABS);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);
    }

    @Test
    public void testUndoClosingCommitsDirtiness() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);

        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onClosingStateChanged(mTab, false);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).setStateForTesting(TabStateAttributes.DirtinessState.UNTIDY);
        observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onClosingStateChanged(mTab, false);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
    }

    @Test
    public void testReparenting() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);

        // Detaching a tab does not mark a tab as needing to be saved.
        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onActivityAttachmentChanged(mTab, null);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);

        WindowAndroid window = mock(WindowAndroid.class);
        // Re-attaching a tab does mark a tab as needing to be saved.
        observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onActivityAttachmentChanged(mTab, window);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.UNTIDY);
    }

    @Test
    public void testNavigationEntryUpdates() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        RewindableIterator<TabObserver> observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onContentChanged(mTab);
        WebContentsObserver webContentsObserver = mWebContentsObserverCaptor.getValue();
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);

        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        webContentsObserver.navigationEntriesChanged();
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.UNTIDY);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).setStateForTesting(TabStateAttributes.DirtinessState.CLEAN);
        observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onNavigationEntriesDeleted(mTab);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);

        TabStateAttributes.from(mTab).setStateForTesting(TabStateAttributes.DirtinessState.CLEAN);
        observers = TabTestUtils.getTabObservers(mTab);
        while (observers.hasNext()) observers.next().onNavigationEntriesAppended(mTab);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver, times(2))
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
    }

    @Test
    public void testRootIdUpdates() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        mTab.setRootId(12);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        TabStateAttributes.from(mTab).clearTabStateDirtiness();

        mTab.setUrl(new GURL(UrlConstants.NTP_URL));
        mTab.setRootId(56);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver, times(2))
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        TabStateAttributes.from(mTab).clearTabStateDirtiness();

        mTab.setUrl(new GURL(UrlConstants.CONTENT_SCHEME + "://hello_world"));
        mTab.setRootId(100);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
    }

    @Test
    public void testTabGroupIdUpdates() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        mTab.setTabGroupId(new Token(1L, 2L));
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        TabStateAttributes.from(mTab).clearTabStateDirtiness();

        mTab.setUrl(new GURL(UrlConstants.NTP_URL));
        mTab.setTabGroupId(null);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver, times(2))
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        TabStateAttributes.from(mTab).clearTabStateDirtiness();

        mTab.setUrl(new GURL(UrlConstants.CONTENT_SCHEME + "://hello_world"));
        mTab.setTabGroupId(new Token(2L, 1L));
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
    }

    @Test
    public void testDuplicateUpdateCalls() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.CLEAN);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.UNTIDY);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.DIRTY);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.DIRTY);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
        verifyNoMoreInteractions(mAttributesObserver);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.CLEAN);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.CLEAN);
        reset(mAttributesObserver);
    }

    @Test
    public void testUpdatesIgnoredDuringRestore() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.CLEAN);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        mTab.setIsBeingRestored(true);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.DIRTY);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        mTab.setIsBeingRestored(false);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.DIRTY);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
    }

    @Test
    public void testDirtyCannotBecomeUntidy() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_FOR_LAZY_LOAD);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());

        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        assertEquals(
                TabStateAttributes.DirtinessState.DIRTY,
                TabStateAttributes.from(mTab).getDirtinessState());
    }

    @Test
    public void testUpdateDirtinessPredicate() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        mTab.setUrl(new GURL(UrlConstants.NTP_URL));
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        mTab.setCanGoForward(false);
        mTab.setCanGoBack(true);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        TabStateAttributes.from(mTab).clearTabStateDirtiness();

        mTab.setCanGoForward(true);
        mTab.setCanGoBack(false);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
        TabStateAttributes.from(mTab).clearTabStateDirtiness();

        mTab.setCanGoForward(false);
        mTab.setCanGoBack(false);
        mTab.setUrl(new GURL(UrlConstants.CONTENT_SCHEME + "://hello_world"));
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        assertEquals(
                TabStateAttributes.DirtinessState.CLEAN,
                TabStateAttributes.from(mTab).getDirtinessState());

        mTab.setUrl(new GURL("https://www.foo.com/"));
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        assertEquals(
                TabStateAttributes.DirtinessState.UNTIDY,
                TabStateAttributes.from(mTab).getDirtinessState());
    }

    @Test
    public void testBatchEdit() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);

        TabStateAttributes.from(mTab).beginBatchEdit();
        mTab.setRootId(1);
        mTab.setTabGroupId(Token.createRandom());
        mTab.setRootId(2);
        mTab.setTabGroupId(null);
        verify(mAttributesObserver, never()).onTabStateDirtinessChanged(eq(mTab), anyInt());
        TabStateAttributes.from(mTab).endBatchEdit();
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).beginBatchEdit();
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.CLEAN);
        verify(mAttributesObserver, never()).onTabStateDirtinessChanged(eq(mTab), anyInt());
        TabStateAttributes.from(mTab).endBatchEdit();
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.CLEAN);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).beginBatchEdit();
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.CLEAN);
        verify(mAttributesObserver, never()).onTabStateDirtinessChanged(eq(mTab), anyInt());
        TabStateAttributes.from(mTab).endBatchEdit();
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.UNTIDY);
        reset(mAttributesObserver);

        TabStateAttributes.from(mTab).beginBatchEdit();
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.CLEAN);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.DIRTY);
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.CLEAN);
        verify(mAttributesObserver, never()).onTabStateDirtinessChanged(eq(mTab), anyInt());
        TabStateAttributes.from(mTab).endBatchEdit();
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        reset(mAttributesObserver);
    }

    @Test
    public void testNestedBatchEdit() {
        TabStateAttributes.createForTab(mTab, TabCreationState.FROZEN_ON_RESTORE);
        TabStateAttributes.from(mTab).addObserver(mAttributesObserver);

        TabStateAttributes.from(mTab).beginBatchEdit();
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.UNTIDY);
        verify(mAttributesObserver, never()).onTabStateDirtinessChanged(eq(mTab), anyInt());

        TabStateAttributes.from(mTab).beginBatchEdit();
        TabStateAttributes.from(mTab).updateIsDirty(TabStateAttributes.DirtinessState.DIRTY);
        verify(mAttributesObserver, never()).onTabStateDirtinessChanged(eq(mTab), anyInt());

        TabStateAttributes.from(mTab).endBatchEdit();
        verify(mAttributesObserver, never()).onTabStateDirtinessChanged(eq(mTab), anyInt());

        TabStateAttributes.from(mTab).endBatchEdit();
        verify(mAttributesObserver)
                .onTabStateDirtinessChanged(mTab, TabStateAttributes.DirtinessState.DIRTY);
        verifyNoMoreInteractions(mAttributesObserver);
    }
}