-
-
Notifications
You must be signed in to change notification settings - Fork 10.2k
test(portal-ui): add portal ui e2e gate and expand coverage #5551
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,99 @@ | ||||||
| # | ||||||
| # Copyright 2026 Apollo Authors | ||||||
| # | ||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| # you may not use this file except in compliance with the License. | ||||||
| # You may obtain a copy of the License at | ||||||
| # | ||||||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||||||
| # | ||||||
| # Unless required by applicable law or agreed to in writing, software | ||||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
| # See the License for the specific language governing permissions and | ||||||
| # limitations under the License. | ||||||
| # | ||||||
|
|
||||||
| name: portal-ui-e2e | ||||||
|
|
||||||
| on: | ||||||
| pull_request: | ||||||
| branches: [ master ] | ||||||
| paths: | ||||||
| - 'apollo-portal/**' | ||||||
| - 'apollo-assembly/**' | ||||||
| - 'e2e/portal-e2e/**' | ||||||
| - 'scripts/sql/**' | ||||||
| - '.github/workflows/portal-ui-e2e.yml' | ||||||
|
|
||||||
| jobs: | ||||||
| portal-ui-e2e: | ||||||
| name: portal-ui-e2e | ||||||
| runs-on: ubuntu-latest | ||||||
| timeout-minutes: 90 | ||||||
| steps: | ||||||
| - uses: actions/checkout@v4 | ||||||
|
|
||||||
| - name: Set up JDK 17 | ||||||
| uses: actions/setup-java@v4 | ||||||
| with: | ||||||
| distribution: temurin | ||||||
| java-version: 17 | ||||||
| cache: maven | ||||||
|
|
||||||
| - name: Set up Node.js 20 | ||||||
| uses: actions/setup-node@v4 | ||||||
| with: | ||||||
| node-version: 20 | ||||||
|
|
||||||
| - name: Cache Playwright browsers | ||||||
| uses: actions/cache@v4 | ||||||
| with: | ||||||
| path: ~/.cache/ms-playwright | ||||||
| key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/portal-e2e/package-lock.json') }} | ||||||
| restore-keys: | | ||||||
| ${{ runner.os }}-playwright- | ||||||
|
|
||||||
| - name: Build Apollo assembly | ||||||
| run: ./mvnw -B -pl apollo-assembly -am -DskipTests package | ||||||
|
|
||||||
| - name: Start Apollo assembly | ||||||
| run: | | ||||||
| JAR=$(ls -1 apollo-assembly/target/apollo-assembly-*.jar | grep -vE 'sources|javadoc' | head -n 1) | ||||||
| SPRING_PROFILES_ACTIVE="github,database-discovery,auth" \ | ||||||
| SPRING_SQL_CONFIG_INIT_MODE="always" \ | ||||||
| SPRING_SQL_PORTAL_INIT_MODE="always" \ | ||||||
| SPRING_CONFIG_DATASOURCE_URL="jdbc:h2:mem:apollo-config-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ | ||||||
| SPRING_PORTAL_DATASOURCE_URL="jdbc:h2:mem:apollo-portal-db;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;BUILTIN_ALIAS_OVERRIDE=TRUE;DATABASE_TO_UPPER=FALSE" \ | ||||||
| java -jar "$JAR" > /tmp/apollo-assembly-run.log 2>&1 & | ||||||
| echo $! > /tmp/apollo-assembly.pid | ||||||
|
|
||||||
| - name: Wait for Apollo readiness | ||||||
| run: ./e2e/portal-e2e/scripts/wait-for-ready.sh | ||||||
|
|
||||||
| - name: Run Playwright UI tests | ||||||
| env: | ||||||
| BASE_URL: http://127.0.0.1:8070 | ||||||
| PLAYWRIGHT_WORKERS: 2 | ||||||
| run: | | ||||||
| cd e2e/portal-e2e | ||||||
| npm ci | ||||||
| npx playwright install chromium | ||||||
|
||||||
| npx playwright install chromium | |
| npx playwright install --with-deps chromium |
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.
Thanks for the suggestion. We are intentionally not applying this in this PR: CI has been stable and we chose to keep the current install mode for lower runtime. If dependency-related flakes appear, we will switch back to --with-deps.
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.
Add --with-deps to playwright install chromium for CI.
ubuntu-latest runners may not have all Chromium system dependencies pre-installed. Without --with-deps, the browser launch can fail with missing shared-library errors. The local run instructions in e2e/README.md (line 62) already include --with-deps.
Suggested fix
- npx playwright install chromium
+ npx playwright install --with-deps chromium🤖 Prompt for AI Agents
In @.github/workflows/portal-ui-e2e.yml around lines 78 - 82, Update the CI step
that calls the Playwright installer: change the command invocation "npx
playwright install chromium" in the run block to include the --with-deps flag
(i.e., "npx playwright install --with-deps chromium") so the installer also
installs required system dependencies for Chromium.
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.
Thanks for the suggestion. We are intentionally not applying this in this PR: CI has been stable and we chose to keep the current install mode for lower runtime. If dependency-related flakes appear, we will switch back to --with-deps.
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.
@nobodyiam, understood! Since your CI has been stable without the flag and you're optimizing for faster runs, keeping the current approach makes sense. If you do encounter any browser launch issues later, the --with-deps flag will be there as a fallback. Thanks for the clarification!
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /* | ||
| * Copyright 2025 Apollo Authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * | ||
| */ | ||
| package com.ctrip.framework.apollo.portal.controller; | ||
|
|
||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertSame; | ||
| import static org.mockito.ArgumentMatchers.eq; | ||
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| import com.ctrip.framework.apollo.common.dto.ClusterDTO; | ||
| import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; | ||
| import com.ctrip.framework.apollo.portal.environment.Env; | ||
| import com.ctrip.framework.apollo.portal.service.ClusterService; | ||
| import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; | ||
| import org.junit.Test; | ||
| import org.junit.runner.RunWith; | ||
| import org.mockito.ArgumentCaptor; | ||
| import org.mockito.Captor; | ||
| import org.mockito.InjectMocks; | ||
| import org.mockito.Mock; | ||
| import org.mockito.junit.MockitoJUnitRunner; | ||
| import org.springframework.http.ResponseEntity; | ||
|
|
||
| @RunWith(MockitoJUnitRunner.class) | ||
| public class ClusterControllerTest { | ||
|
|
||
| @Mock | ||
| private ClusterService clusterService; | ||
|
|
||
| @Mock | ||
| private UserInfoHolder userInfoHolder; | ||
|
|
||
| @InjectMocks | ||
| private ClusterController clusterController; | ||
|
|
||
| @Captor | ||
| private ArgumentCaptor<ClusterDTO> clusterCaptor; | ||
|
|
||
| @Test | ||
| public void shouldCreateClusterWithCurrentOperator() { | ||
| ClusterDTO toCreate = new ClusterDTO(); | ||
| toCreate.setAppId("SampleApp"); | ||
| toCreate.setName("sampleCluster"); | ||
|
|
||
| ClusterDTO created = new ClusterDTO(); | ||
| created.setAppId("SampleApp"); | ||
| created.setName("sampleCluster"); | ||
|
|
||
| when(userInfoHolder.getUser()).thenReturn(new UserInfo("apollo")); | ||
| when(clusterService.createCluster(eq(Env.DEV), clusterCaptor.capture())).thenReturn(created); | ||
|
|
||
| ClusterDTO result = clusterController.createCluster("SampleApp", "DEV", toCreate); | ||
|
|
||
| assertSame(created, result); | ||
| ClusterDTO captured = clusterCaptor.getValue(); | ||
| assertEquals("apollo", captured.getDataChangeCreatedBy()); | ||
| assertEquals("apollo", captured.getDataChangeLastModifiedBy()); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldDeleteClusterByEnvAndName() { | ||
| ResponseEntity<Void> response = | ||
| clusterController.deleteCluster("SampleApp", "DEV", "sampleCluster"); | ||
|
|
||
| assertEquals(200, response.getStatusCodeValue()); | ||
| verify(clusterService).deleteCluster(Env.DEV, "SampleApp", "sampleCluster"); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldLoadClusterFromService() { | ||
| ClusterDTO loaded = new ClusterDTO(); | ||
| loaded.setName("sampleCluster"); | ||
| when(clusterService.loadCluster("SampleApp", Env.DEV, "sampleCluster")).thenReturn(loaded); | ||
|
|
||
| ClusterDTO result = clusterController.loadCluster("SampleApp", "DEV", "sampleCluster"); | ||
|
|
||
| assertSame(loaded, result); | ||
| verify(clusterService).loadCluster("SampleApp", Env.DEV, "sampleCluster"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| /* | ||
| * Copyright 2025 Apollo Authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * | ||
| */ | ||
| package com.ctrip.framework.apollo.portal.controller; | ||
|
|
||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertTrue; | ||
| import static org.mockito.ArgumentMatchers.any; | ||
| import static org.mockito.ArgumentMatchers.eq; | ||
| import static org.mockito.Mockito.doAnswer; | ||
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| import com.ctrip.framework.apollo.common.dto.ItemDTO; | ||
| import com.ctrip.framework.apollo.common.exception.ServiceException; | ||
| import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; | ||
| import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; | ||
| import com.ctrip.framework.apollo.portal.environment.Env; | ||
| import com.ctrip.framework.apollo.portal.service.ConfigsExportService; | ||
| import com.ctrip.framework.apollo.portal.service.NamespaceService; | ||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import javax.servlet.ServletOutputStream; | ||
| import javax.servlet.WriteListener; | ||
| import javax.servlet.http.HttpServletRequest; | ||
| import javax.servlet.http.HttpServletResponse; | ||
| import org.junit.Test; | ||
| import org.junit.runner.RunWith; | ||
| import org.mockito.InjectMocks; | ||
| import org.mockito.Mock; | ||
| import org.mockito.junit.MockitoJUnitRunner; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.mock.web.MockHttpServletRequest; | ||
| import org.springframework.mock.web.MockHttpServletResponse; | ||
|
|
||
| @RunWith(MockitoJUnitRunner.class) | ||
| public class ConfigsExportControllerTest { | ||
|
|
||
| @Mock | ||
| private ConfigsExportService configsExportService; | ||
|
|
||
| @Mock | ||
| private NamespaceService namespaceService; | ||
|
|
||
| @InjectMocks | ||
| private ConfigsExportController configsExportController; | ||
|
|
||
| @Test | ||
| public void shouldExportNamespaceWithPropertiesSuffixWhenMissing() { | ||
| NamespaceBO namespace = new NamespaceBO(); | ||
| namespace.setFormat("properties"); | ||
| namespace.setItems(Collections.singletonList(itemBO("timeout", "100"))); | ||
|
|
||
| when(namespaceService.loadNamespaceBO("SampleApp", Env.DEV, "default", "application", true, | ||
| false)).thenReturn(namespace); | ||
|
|
||
| MockHttpServletResponse response = new MockHttpServletResponse(); | ||
|
|
||
| configsExportController.exportItems("SampleApp", "DEV", "default", "application", response); | ||
|
|
||
| assertEquals("attachment;filename=application.properties", | ||
| response.getHeader(HttpHeaders.CONTENT_DISPOSITION)); | ||
| assertTrue(new String(response.getContentAsByteArray()).contains("\"key\":\"timeout\"")); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldExportNamespaceKeepOriginalSuffixWhenFormatIsValid() { | ||
| NamespaceBO namespace = new NamespaceBO(); | ||
| namespace.setFormat("yml"); | ||
| namespace.setItems(Collections.singletonList(itemBO("content", "a: b"))); | ||
|
|
||
| when(namespaceService.loadNamespaceBO("SampleApp", Env.DEV, "default", "application.yml", true, | ||
| false)).thenReturn(namespace); | ||
|
|
||
| MockHttpServletResponse response = new MockHttpServletResponse(); | ||
|
|
||
| configsExportController.exportItems("SampleApp", "DEV", "default", "application.yml", response); | ||
|
|
||
| assertEquals("attachment;filename=application.yml", | ||
| response.getHeader(HttpHeaders.CONTENT_DISPOSITION)); | ||
| assertTrue(new String(response.getContentAsByteArray()).contains("\"key\":\"content\"")); | ||
| } | ||
|
|
||
| @Test(expected = ServiceException.class) | ||
| public void shouldWrapExportNamespaceIOExceptionAsServiceException() throws IOException { | ||
| NamespaceBO namespace = new NamespaceBO(); | ||
| namespace.setFormat("properties"); | ||
| namespace.setItems(Collections.singletonList(itemBO("timeout", "100"))); | ||
|
|
||
| when(namespaceService.loadNamespaceBO("SampleApp", Env.DEV, "default", "application", true, | ||
| false)).thenReturn(namespace); | ||
|
|
||
| HttpServletResponse response = org.mockito.Mockito.mock(HttpServletResponse.class); | ||
| when(response.getOutputStream()).thenReturn(new FailingServletOutputStream()); | ||
|
|
||
| configsExportController.exportItems("SampleApp", "DEV", "default", "application", response); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldExportAllConfigsWithParsedEnvs() throws IOException { | ||
| MockHttpServletRequest request = new MockHttpServletRequest(); | ||
| request.setRemoteAddr("127.0.0.1"); | ||
| request.setRemoteHost("localhost"); | ||
| MockHttpServletResponse response = new MockHttpServletResponse(); | ||
|
|
||
| doAnswer(invocation -> { | ||
| OutputStream outputStream = invocation.getArgument(0); | ||
| outputStream.write("ok".getBytes()); | ||
| return null; | ||
| }).when(configsExportService).exportData(any(OutputStream.class), | ||
| eq(Arrays.asList(Env.DEV, Env.FAT))); | ||
|
|
||
| configsExportController.exportAll("DEV,FAT", request, response); | ||
|
|
||
| verify(configsExportService).exportData(any(OutputStream.class), | ||
| eq(Arrays.asList(Env.DEV, Env.FAT))); | ||
| assertTrue(response.getHeader(HttpHeaders.CONTENT_DISPOSITION) | ||
| .startsWith("attachment;filename=apollo_config_export_")); | ||
| assertEquals("ok", response.getContentAsString()); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldExportAppConfigByEnvAndCluster() throws IOException { | ||
| HttpServletRequest request = new MockHttpServletRequest(); | ||
| MockHttpServletResponse response = new MockHttpServletResponse(); | ||
|
|
||
| doAnswer(invocation -> { | ||
| OutputStream outputStream = invocation.getArgument(3); | ||
| outputStream.write("app".getBytes()); | ||
| return null; | ||
| }).when(configsExportService).exportAppConfigByEnvAndCluster(eq("SampleApp"), eq(Env.DEV), | ||
| eq("default"), any(OutputStream.class)); | ||
|
|
||
| configsExportController.exportAppConfig("SampleApp", "DEV", "default", request, response); | ||
|
|
||
| verify(configsExportService).exportAppConfigByEnvAndCluster(eq("SampleApp"), eq(Env.DEV), | ||
| eq("default"), any(OutputStream.class)); | ||
| assertTrue(response.getHeader(HttpHeaders.CONTENT_DISPOSITION) | ||
| .startsWith("attachment;filename=SampleApp+DEV+default+")); | ||
| assertEquals("app", response.getContentAsString()); | ||
| } | ||
|
|
||
| private ItemBO itemBO(String key, String value) { | ||
| ItemDTO itemDTO = new ItemDTO(); | ||
| itemDTO.setKey(key); | ||
| itemDTO.setValue(value); | ||
| ItemBO itemBO = new ItemBO(); | ||
| itemBO.setItem(itemDTO); | ||
| return itemBO; | ||
| } | ||
|
|
||
| private static class FailingServletOutputStream extends ServletOutputStream { | ||
|
|
||
| @Override | ||
| public void write(int b) throws IOException { | ||
| throw new IOException("forced write failure"); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isReady() { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public void setWriteListener(WriteListener writeListener) { | ||
| // no-op | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.