完善e2e测试与后端测试,微信小程序端UI层测试暂未完成
This commit was merged in pull request #52.
This commit is contained in:
+175
@@ -0,0 +1,175 @@
|
|||||||
|
package cn.novalon.gym.manage.auth.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
||||||
|
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
|
||||||
|
import cn.novalon.gym.manage.auth.dto.SendCodeRequest;
|
||||||
|
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
|
||||||
|
import cn.novalon.gym.manage.auth.vo.PhoneLoginVO;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PhoneAuthHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PhoneAuthService phoneAuthService;
|
||||||
|
|
||||||
|
private PhoneAuthHandler phoneAuthHandler;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
phoneAuthHandler = new PhoneAuthHandler(phoneAuthService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== oneClickLogin ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void oneClickLogin_shouldReturnOkWithLoginResult() {
|
||||||
|
PhoneLoginVO loginVO = new PhoneLoginVO();
|
||||||
|
loginVO.setAccessToken("test-jwt-token");
|
||||||
|
loginVO.setPhone("13800138000");
|
||||||
|
|
||||||
|
PhoneLoginDto dto = new PhoneLoginDto();
|
||||||
|
dto.setAccessToken("dcloud-access-token");
|
||||||
|
|
||||||
|
when(phoneAuthService.oneClickLogin(any(PhoneLoginDto.class))).thenReturn(Mono.just(loginVO));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(dto));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = phoneAuthHandler.oneClickLogin(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(phoneAuthService).oneClickLogin(any(PhoneLoginDto.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void oneClickLogin_shouldPropagateServiceError() {
|
||||||
|
PhoneLoginDto dto = new PhoneLoginDto();
|
||||||
|
dto.setAccessToken("invalid-token");
|
||||||
|
|
||||||
|
when(phoneAuthService.oneClickLogin(any(PhoneLoginDto.class)))
|
||||||
|
.thenReturn(Mono.error(new RuntimeException("Auth failed")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(dto));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = phoneAuthHandler.oneClickLogin(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectError(RuntimeException.class)
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== sendSmsCode ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendSmsCode_shouldReturnOkWithSuccessTrue() {
|
||||||
|
SendCodeRequest sendCodeRequest = new SendCodeRequest();
|
||||||
|
sendCodeRequest.setPhone("13800138000");
|
||||||
|
|
||||||
|
when(phoneAuthService.sendSmsCode("13800138000")).thenReturn(Mono.just(true));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(sendCodeRequest));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = phoneAuthHandler.sendSmsCode(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendSmsCode_shouldReturnOkWithSuccessFalseWhenServiceReturnsFalse() {
|
||||||
|
SendCodeRequest sendCodeRequest = new SendCodeRequest();
|
||||||
|
sendCodeRequest.setPhone("13800138000");
|
||||||
|
|
||||||
|
when(phoneAuthService.sendSmsCode("13800138000")).thenReturn(Mono.just(false));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(sendCodeRequest));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = phoneAuthHandler.sendSmsCode(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendSmsCode_shouldPropagateError() {
|
||||||
|
SendCodeRequest sendCodeRequest = new SendCodeRequest();
|
||||||
|
sendCodeRequest.setPhone("13800138000");
|
||||||
|
|
||||||
|
when(phoneAuthService.sendSmsCode("13800138000"))
|
||||||
|
.thenReturn(Mono.error(new RuntimeException("SMS service unavailable")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(sendCodeRequest));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = phoneAuthHandler.sendSmsCode(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectError(RuntimeException.class)
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== codeLogin ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void codeLogin_shouldReturnOkWithLoginResult() {
|
||||||
|
PhoneLoginVO loginVO = new PhoneLoginVO();
|
||||||
|
loginVO.setAccessToken("test-jwt-token");
|
||||||
|
|
||||||
|
PhoneCodeLoginDto dto = new PhoneCodeLoginDto();
|
||||||
|
dto.setPhone("13800138000");
|
||||||
|
dto.setCode("123456");
|
||||||
|
|
||||||
|
when(phoneAuthService.codeLogin(any(PhoneCodeLoginDto.class))).thenReturn(Mono.just(loginVO));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(dto));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = phoneAuthHandler.codeLogin(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void codeLogin_shouldPropagateServiceError() {
|
||||||
|
PhoneCodeLoginDto dto = new PhoneCodeLoginDto();
|
||||||
|
dto.setPhone("13800138000");
|
||||||
|
dto.setCode("wrong-code");
|
||||||
|
|
||||||
|
when(phoneAuthService.codeLogin(any(PhoneCodeLoginDto.class)))
|
||||||
|
.thenReturn(Mono.error(new RuntimeException("Invalid code")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(dto));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = phoneAuthHandler.codeLogin(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectError(RuntimeException.class)
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
package cn.novalon.gym.manage.checkin.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.checkIn.handler.CheckInHandler;
|
||||||
|
import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl;
|
||||||
|
import cn.novalon.gym.manage.checkIn.vo.QRCodeVo;
|
||||||
|
import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO;
|
||||||
|
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
|
||||||
|
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CheckInHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AuthUtil authUtil;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CheckServiceImpl checkService;
|
||||||
|
|
||||||
|
private CheckInHandler checkInHandler;
|
||||||
|
|
||||||
|
private static final Long MEMBER_ID = 10001L;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
checkInHandler = new CheckInHandler(authUtil, checkService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== checkIn ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkIn_shouldReturnOk() {
|
||||||
|
Map<String, Object> body = Map.of("qrContent", "checkin:member:10001");
|
||||||
|
|
||||||
|
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||||
|
when(checkService.checkIn(MEMBER_ID, "checkin:member:10001")).thenReturn(Mono.just("签到成功"));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(body));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = checkInHandler.checkIn(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
verify(checkService).checkIn(MEMBER_ID, "checkin:member:10001");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkIn_shouldReturnBadRequestOnError() {
|
||||||
|
Map<String, Object> body = Map.of("qrContent", "invalid-content");
|
||||||
|
|
||||||
|
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||||
|
when(checkService.checkIn(MEMBER_ID, "invalid-content"))
|
||||||
|
.thenReturn(Mono.error(new RuntimeException("Invalid QR code")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(body));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = checkInHandler.checkIn(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getQRCode ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getQRCode_shouldReturnOkWithQRCode() {
|
||||||
|
QRCodeVo qrCode = new QRCodeVo("base64content", false, "qr-content", 200, 200, LocalDate.now());
|
||||||
|
|
||||||
|
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||||
|
when(checkService.getQRCode(MEMBER_ID)).thenReturn(Mono.just(qrCode));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = checkInHandler.getQRCode(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getSignInRecords ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSignInRecords_shouldReturnOkWithRecords() {
|
||||||
|
List<SignInRecordVO> records = List.of(createTestRecord());
|
||||||
|
|
||||||
|
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||||
|
when(checkService.getSignInRecords(eq(MEMBER_ID), any(LocalDate.class), any(LocalDate.class)))
|
||||||
|
.thenReturn(Flux.fromIterable(records));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("startDate", "2025-01-01")
|
||||||
|
.queryParam("endDate", "2025-01-31")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = checkInHandler.getSignInRecords(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getSignInStatistics ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSignInStatistics_shouldReturnOkWithStats() {
|
||||||
|
SignInStatsVO stats = new SignInStatsVO();
|
||||||
|
|
||||||
|
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||||
|
when(checkService.getSignInStats(eq(MEMBER_ID), any(LocalDate.class), any(LocalDate.class)))
|
||||||
|
.thenReturn(Mono.just(stats));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("startDate", "2025-01-01")
|
||||||
|
.queryParam("endDate", "2025-01-31")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = checkInHandler.getSignInStatistics(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getDailySignInStats ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getDailySignInStats_shouldReturnOkWithDailyStats() {
|
||||||
|
SignInStatsVO stats = new SignInStatsVO();
|
||||||
|
|
||||||
|
when(checkService.getDailySignInStats(any(LocalDate.class))).thenReturn(Mono.just(stats));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("date", "2025-01-15")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = checkInHandler.getDailySignInStats(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== helper ====================
|
||||||
|
|
||||||
|
private SignInRecordVO createTestRecord() {
|
||||||
|
SignInRecordVO record = new SignInRecordVO();
|
||||||
|
record.setId(1L);
|
||||||
|
record.setMemberId(MEMBER_ID);
|
||||||
|
record.setSignInTime(LocalDateTime.now());
|
||||||
|
record.setSignInType("QR_CODE");
|
||||||
|
record.setSignInStatus("SUCCESS");
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
}
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.coach.service.CoachCourseService;
|
||||||
|
import cn.novalon.gym.manage.sys.core.domain.SysUser;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import jakarta.validation.Validator;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CoachHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CoachCourseService coachCourseService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Validator validator;
|
||||||
|
|
||||||
|
private CoachHandler coachHandler;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
coachHandler = new CoachHandler(coachCourseService, validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getAllCoaches ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllCoaches_shouldReturnOkWithCoachList() {
|
||||||
|
SysUser coach = mock(SysUser.class);
|
||||||
|
when(coachCourseService.getAllCoaches()).thenReturn(Flux.just(coach));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.getAllCoaches(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
verify(coachCourseService).getAllCoaches();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllCoaches_shouldReturnOkWhenEmptyList() {
|
||||||
|
when(coachCourseService.getAllCoaches()).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.getAllCoaches(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getCoachCourses ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCoachCourses_shouldReturnOkWithCourses() {
|
||||||
|
GroupCourse course = mock(GroupCourse.class);
|
||||||
|
when(coachCourseService.getCoachCourses(anyLong())).thenReturn(Flux.just(course));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.getCoachCourses(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCoachCourses_shouldReturnOkWhenEmpty() {
|
||||||
|
when(coachCourseService.getCoachCourses(anyLong())).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.getCoachCourses(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== disableCoach ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void disableCoach_shouldReturnOkWhenDisabled() {
|
||||||
|
when(coachCourseService.disableCoach(anyLong())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.disableCoach(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void disableCoach_shouldReturnBadRequestOnError() {
|
||||||
|
when(coachCourseService.disableCoach(anyLong()))
|
||||||
|
.thenReturn(Mono.error(new RuntimeException("Coach not found")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.disableCoach(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getViolationCounts ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getViolationCounts_shouldReturnOk() {
|
||||||
|
when(coachCourseService.getViolationCounts()).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.getViolationCounts(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getCoachViolations ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCoachViolations_shouldReturnOkWithViolations() {
|
||||||
|
when(coachCourseService.getCoachViolations(anyLong())).thenReturn(Flux.just(Map.of("violationId", 1, "reason", "迟到")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.getCoachViolations(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCoachViolations_shouldReturnEmptyListWhenNone() {
|
||||||
|
when(coachCourseService.getCoachViolations(anyLong())).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = coachHandler.getCoachViolations(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
package cn.novalon.gym.manage.datacount.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.datacount.domain.*;
|
||||||
|
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DataStatisticsHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IDataStatisticsService dataStatisticsService;
|
||||||
|
|
||||||
|
private DataStatisticsHandler handler;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
handler = new DataStatisticsHandler();
|
||||||
|
java.lang.reflect.Field field = DataStatisticsHandler.class.getDeclaredField("dataStatisticsService");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(handler, dataStatisticsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getStatisticsSummary ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getStatisticsSummary_shouldReturnOkWithSummary() {
|
||||||
|
StatisticsSummary summary = createTestSummary();
|
||||||
|
when(dataStatisticsService.getStatisticsSummaryWithCache(any(StatisticsQuery.class))).thenReturn(Mono.just(summary));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "DAY")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getStatisticsSummary(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getStatisticsSummary_shouldReturnOkEvenOnError() {
|
||||||
|
when(dataStatisticsService.getStatisticsSummaryWithCache(any(StatisticsQuery.class)))
|
||||||
|
.thenReturn(Mono.error(new RuntimeException("Service error")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "DAY")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Error handler returns empty/default summary with 200 OK
|
||||||
|
Mono<ServerResponse> result = handler.getStatisticsSummary(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getMemberStatistics ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMemberStatistics_shouldReturnOk() {
|
||||||
|
MemberStatistics stats = new MemberStatistics();
|
||||||
|
when(dataStatisticsService.getMemberStatistics(any(StatisticsQuery.class))).thenReturn(Mono.just(stats));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "WEEK")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getMemberStatistics(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMemberStatistics_shouldReturnOkEvenOnError() {
|
||||||
|
when(dataStatisticsService.getMemberStatistics(any(StatisticsQuery.class)))
|
||||||
|
.thenReturn(Mono.error(new RuntimeException("Service error")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "WEEK")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getMemberStatistics(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getBookingStatistics ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBookingStatistics_shouldReturnOk() {
|
||||||
|
BookingStatistics stats = new BookingStatistics();
|
||||||
|
when(dataStatisticsService.getBookingStatistics(any(StatisticsQuery.class))).thenReturn(Mono.just(stats));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "MONTH")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getBookingStatistics(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getSignInStatistics ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSignInStatistics_shouldReturnOk() {
|
||||||
|
SignInStatistics stats = new SignInStatistics();
|
||||||
|
when(dataStatisticsService.getSignInStatistics(any(StatisticsQuery.class))).thenReturn(Mono.just(stats));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "MONTH")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getSignInStatistics(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== queryHistoricalStatistics ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryHistoricalStatistics_shouldReturnOkWithList() {
|
||||||
|
when(dataStatisticsService.queryHistoricalStatistics(any(StatisticsQuery.class)))
|
||||||
|
.thenReturn(Flux.just(createTestDataStatistics()));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "YEAR")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.queryHistoricalStatistics(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryHistoricalStatistics_shouldReturnOkWhenEmpty() {
|
||||||
|
when(dataStatisticsService.queryHistoricalStatistics(any(StatisticsQuery.class)))
|
||||||
|
.thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "YEAR")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.queryHistoricalStatistics(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== exportStatistics ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exportStatistics_shouldReturnOkWithExcelContent() {
|
||||||
|
byte[] excelData = "mock-excel-content".getBytes();
|
||||||
|
when(dataStatisticsService.exportStatistics(any(StatisticsQuery.class))).thenReturn(Mono.just(excelData));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.queryParam("periodType", "MONTH")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.exportStatistics(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== buildQueryFromRequest (via parameterized tests) ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getStatisticsSummary_shouldUseDefaultPeriodWhenMissing() {
|
||||||
|
StatisticsSummary summary = createTestSummary();
|
||||||
|
when(dataStatisticsService.getStatisticsSummaryWithCache(any(StatisticsQuery.class))).thenReturn(Mono.just(summary));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getStatisticsSummary(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== helper ====================
|
||||||
|
|
||||||
|
private StatisticsSummary createTestSummary() {
|
||||||
|
StatisticsSummary summary = new StatisticsSummary();
|
||||||
|
summary.setMemberStatistics(new MemberStatistics());
|
||||||
|
summary.setBookingStatistics(new BookingStatistics());
|
||||||
|
summary.setSignInStatistics(new SignInStatistics());
|
||||||
|
summary.setCoachStatistics(new CoachStatistics());
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataStatistics createTestDataStatistics() {
|
||||||
|
return DataStatistics.builder()
|
||||||
|
.statType("MEMBER")
|
||||||
|
.periodType("DAY")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+227
@@ -0,0 +1,227 @@
|
|||||||
|
package cn.novalon.gym.manage.groupcourse.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.vo.GroupCourseVO;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.validation.Validator;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class GroupCourseHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IGroupCourseService groupCourseService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Validator validator;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private GroupCourseHandler handler;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
handler = new GroupCourseHandler(groupCourseService, validator, redisUtil, objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getAllGroupCourse ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllGroupCourse_shouldReturnOkWithCourses() {
|
||||||
|
GroupCourseVO vo1 = mock(GroupCourseVO.class);
|
||||||
|
GroupCourseVO vo2 = mock(GroupCourseVO.class);
|
||||||
|
when(groupCourseService.findAllAsVO(false)).thenReturn(Flux.just(vo1, vo2));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
Mono<ServerResponse> result = handler.getAllGroupCourse(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
verify(groupCourseService).findAllAsVO(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllGroupCourse_shouldReturnOkWhenEmpty() {
|
||||||
|
when(groupCourseService.findAllAsVO(false)).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
Mono<ServerResponse> result = handler.getAllGroupCourse(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getGroupCourseById ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getGroupCourseById_shouldReturnOkWhenFound() {
|
||||||
|
GroupCourse course = createTestCourse(1L, "瑜伽课");
|
||||||
|
when(groupCourseService.findById(1L)).thenReturn(Mono.just(course));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
Mono<ServerResponse> result = handler.getGroupCourseById(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getGroupCourseById_shouldReturnNotFound() {
|
||||||
|
when(groupCourseService.findById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
Mono<ServerResponse> result = handler.getGroupCourseById(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getGroupCourseDetailById ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getGroupCourseDetailById_shouldReturnOkWhenFound() {
|
||||||
|
GroupCourseDetail detail = mock(GroupCourseDetail.class);
|
||||||
|
when(groupCourseService.findDetailById(1L)).thenReturn(Mono.just(detail));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
Mono<ServerResponse> result = handler.getGroupCourseDetailById(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getGroupCourseDetailById_shouldReturnNotFound() {
|
||||||
|
when(groupCourseService.findDetailById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
Mono<ServerResponse> result = handler.getGroupCourseDetailById(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== cancelGroupCourse ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cancelGroupCourse_shouldReturnOkWhenCancelled() {
|
||||||
|
GroupCourse cancelled = createTestCourse(1L, "瑜伽课");
|
||||||
|
cancelled.setStatus(2L);
|
||||||
|
when(groupCourseService.cancel(1L)).thenReturn(Mono.just(cancelled));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
Mono<ServerResponse> result = handler.cancelGroupCourse(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cancelGroupCourse_shouldReturnNotFound() {
|
||||||
|
when(groupCourseService.cancel(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
Mono<ServerResponse> result = handler.cancelGroupCourse(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== deleteGroupCourse ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteGroupCourse_shouldReturnOkWhenDeleted() {
|
||||||
|
when(groupCourseService.delete(1L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
Mono<ServerResponse> result = handler.deleteGroupCourse(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteGroupCourse_shouldReturnNotFound() {
|
||||||
|
when(groupCourseService.delete(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
Mono<ServerResponse> result = handler.deleteGroupCourse(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== signIn ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signIn_shouldReturnOk() {
|
||||||
|
GroupCourse course = createTestCourse(1L, "瑜伽课");
|
||||||
|
when(validator.validate(any())).thenReturn(java.util.Collections.emptySet());
|
||||||
|
when(groupCourseService.signIn(eq(1L), eq(10001L))).thenReturn(Mono.just(course));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("courseId", "1")
|
||||||
|
.body(Mono.just(java.util.Map.of("memberId", 10001L, "courseId", 1L)));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.signIn(request);
|
||||||
|
|
||||||
|
ServerResponse response = result.block();
|
||||||
|
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== helper ====================
|
||||||
|
|
||||||
|
private GroupCourse createTestCourse(Long id, String courseName) {
|
||||||
|
GroupCourse course = new GroupCourse();
|
||||||
|
course.setId(id);
|
||||||
|
course.setCourseName(courseName);
|
||||||
|
course.setCourseType(1L);
|
||||||
|
course.setCoachId(1L);
|
||||||
|
course.setStartTime(LocalDateTime.now().plusDays(1));
|
||||||
|
course.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1));
|
||||||
|
course.setLocation("101室");
|
||||||
|
course.setMaxMembers(20);
|
||||||
|
course.setCurrentMembers(5);
|
||||||
|
course.setStatus(0L);
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package cn.novalon.gym.manage.file.core.domain;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class SysFileTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateSysFileWithCorrectProperties() {
|
||||||
|
SysFile file = new SysFile();
|
||||||
|
file.setId(1L);
|
||||||
|
file.setFileName("test.png");
|
||||||
|
file.setFileType("image/png");
|
||||||
|
file.setFileSize(1024L);
|
||||||
|
file.setFilePath("/uploads/test.png");
|
||||||
|
file.setCreatedAt(LocalDateTime.of(2025, 1, 1, 10, 0, 0));
|
||||||
|
|
||||||
|
assertThat(file.getId()).isEqualTo(1L);
|
||||||
|
assertThat(file.getFileName()).isEqualTo("test.png");
|
||||||
|
assertThat(file.getFileType()).isEqualTo("image/png");
|
||||||
|
assertThat(file.getFileSize()).isEqualTo(1024L);
|
||||||
|
assertThat(file.getFilePath()).isEqualTo("/uploads/test.png");
|
||||||
|
assertThat(file.getCreatedAt()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleNullValues() {
|
||||||
|
SysFile file = new SysFile();
|
||||||
|
|
||||||
|
assertThat(file.getId()).isNull();
|
||||||
|
assertThat(file.getFileName()).isNull();
|
||||||
|
assertThat(file.getFileType()).isNull();
|
||||||
|
assertThat(file.getFileSize()).isNull();
|
||||||
|
assertThat(file.getFilePath()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSetAndGetAllProperties() {
|
||||||
|
SysFile file = new SysFile();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
file.setId(100L);
|
||||||
|
file.setFileName("document.pdf");
|
||||||
|
file.setFileType("application/pdf");
|
||||||
|
file.setFileSize(20480L);
|
||||||
|
file.setFilePath("/files/2025/document.pdf");
|
||||||
|
file.setCreatedAt(now);
|
||||||
|
file.setDeletedAt(now);
|
||||||
|
|
||||||
|
assertThat(file.getId()).isEqualTo(100L);
|
||||||
|
assertThat(file.getFileSize()).isEqualTo(20480L);
|
||||||
|
assertThat(file.getDeletedAt()).isEqualTo(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.core.service.impl;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||||
|
import cn.novalon.gym.manage.notify.core.repository.IBannerRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class BannerServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IBannerRepository bannerRepository;
|
||||||
|
|
||||||
|
private BannerServiceImpl bannerService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
bannerService = new BannerServiceImpl(bannerRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getAllBanners ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllBanners_shouldReturnNonDeletedBanners() {
|
||||||
|
Banner banner = createTestBanner(1L, "轮播图1");
|
||||||
|
when(bannerRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(banner));
|
||||||
|
|
||||||
|
Flux<Banner> result = bannerService.getAllBanners();
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextCount(1)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(bannerRepository).findByDeletedAtIsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllBanners_shouldReturnEmptyWhenNone() {
|
||||||
|
when(bannerRepository.findByDeletedAtIsNull()).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
Flux<Banner> result = bannerService.getAllBanners();
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getActiveBanners ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getActiveBanners_shouldReturnActiveBanners() {
|
||||||
|
when(bannerRepository.findActiveBanners()).thenReturn(Flux.just(createTestBanner(1L, "活跃")));
|
||||||
|
|
||||||
|
Flux<Banner> result = bannerService.getActiveBanners();
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextCount(1)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(bannerRepository).findActiveBanners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getBannerById ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBannerById_shouldReturnBannerWhenFound() {
|
||||||
|
when(bannerRepository.findById(1L)).thenReturn(Mono.just(createTestBanner(1L, "轮播图1")));
|
||||||
|
|
||||||
|
Mono<Banner> result = bannerService.getBannerById(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextCount(1)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBannerById_shouldReturnEmptyWhenNotFound() {
|
||||||
|
when(bannerRepository.findById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<Banner> result = bannerService.getBannerById(999L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== createBanner ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createBanner_shouldSaveAndReturn() {
|
||||||
|
Banner banner = createTestBanner(null, "新轮播图");
|
||||||
|
when(bannerRepository.save(any(Banner.class)))
|
||||||
|
.thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||||
|
|
||||||
|
Mono<Banner> result = bannerService.createBanner(banner);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(b -> b.getTitle().equals("新轮播图") && b.getCreatedAt() != null)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(bannerRepository).save(banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== updateBanner ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateBanner_shouldUpdateAndReturn() {
|
||||||
|
Banner existing = createTestBanner(1L, "旧标题");
|
||||||
|
Banner update = createTestBanner(1L, "新标题");
|
||||||
|
when(bannerRepository.findById(1L)).thenReturn(Mono.just(existing));
|
||||||
|
when(bannerRepository.save(any(Banner.class)))
|
||||||
|
.thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||||
|
|
||||||
|
Mono<Banner> result = bannerService.updateBanner(1L, update);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(b -> b.getTitle().equals("新标题") && b.getUpdatedAt() != null)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(bannerRepository).save(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateBanner_shouldReturnEmptyWhenNotFound() {
|
||||||
|
Banner update = createTestBanner(999L, "新标题");
|
||||||
|
when(bannerRepository.findById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<Banner> result = bannerService.updateBanner(999L, update);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(bannerRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== deleteBanner ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteBanner_shouldSoftDelete() {
|
||||||
|
Banner existing = createTestBanner(1L, "轮播图1");
|
||||||
|
when(bannerRepository.findById(1L)).thenReturn(Mono.just(existing));
|
||||||
|
when(bannerRepository.save(any(Banner.class))).thenReturn(Mono.just(existing));
|
||||||
|
|
||||||
|
Mono<Void> result = bannerService.deleteBanner(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(bannerRepository).save(existing);
|
||||||
|
assertThat(existing.getDeletedAt()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteBanner_shouldCompleteEmptyWhenNotFound() {
|
||||||
|
when(bannerRepository.findById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<Void> result = bannerService.deleteBanner(999L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== helper ====================
|
||||||
|
|
||||||
|
private Banner createTestBanner(Long id, String title) {
|
||||||
|
Banner banner = new Banner();
|
||||||
|
banner.setId(id);
|
||||||
|
banner.setImageUrl("https://example.com/banner.jpg");
|
||||||
|
banner.setTitle(title);
|
||||||
|
banner.setSubtitle("副标题");
|
||||||
|
banner.setSortOrder(1);
|
||||||
|
banner.setIsActive("1");
|
||||||
|
banner.setCreatedAt(LocalDateTime.now());
|
||||||
|
return banner;
|
||||||
|
}
|
||||||
|
}
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.core.service.impl;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.SysNotice;
|
||||||
|
import cn.novalon.gym.manage.notify.core.repository.ISysNoticeRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SysNoticeServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ISysNoticeRepository noticeRepository;
|
||||||
|
|
||||||
|
private SysNoticeServiceImpl noticeService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
noticeService = new SysNoticeServiceImpl(noticeRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getAllNotices ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllNotices_shouldReturnNonDeletedNotices() {
|
||||||
|
SysNotice notice = createTestNotice(1L, "公告1");
|
||||||
|
when(noticeRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(notice));
|
||||||
|
|
||||||
|
Flux<SysNotice> result = noticeService.getAllNotices();
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(n -> n.getId().equals(1L) && n.getNoticeTitle().equals("公告1"))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(noticeRepository).findByDeletedAtIsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllNotices_shouldReturnEmptyWhenNone() {
|
||||||
|
when(noticeRepository.findByDeletedAtIsNull()).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
Flux<SysNotice> result = noticeService.getAllNotices();
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getNoticeById ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNoticeById_shouldReturnNoticeWhenFound() {
|
||||||
|
SysNotice notice = createTestNotice(1L, "公告1");
|
||||||
|
when(noticeRepository.findById(1L)).thenReturn(Mono.just(notice));
|
||||||
|
|
||||||
|
Mono<SysNotice> result = noticeService.getNoticeById(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(n -> n.getId().equals(1L))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNoticeById_shouldReturnEmptyWhenNotFound() {
|
||||||
|
when(noticeRepository.findById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<SysNotice> result = noticeService.getNoticeById(999L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== createNotice ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createNotice_shouldSaveAndReturnNotice() {
|
||||||
|
SysNotice notice = createTestNotice(null, "新公告");
|
||||||
|
when(noticeRepository.save(any(SysNotice.class)))
|
||||||
|
.thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||||
|
|
||||||
|
Mono<SysNotice> result = noticeService.createNotice(notice);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(n -> n.getNoticeTitle().equals("新公告"))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(noticeRepository).save(notice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== updateNotice ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateNotice_shouldUpdateAndReturn() {
|
||||||
|
SysNotice existing = createTestNotice(1L, "旧标题");
|
||||||
|
SysNotice update = createTestNotice(1L, "新标题");
|
||||||
|
when(noticeRepository.findById(1L)).thenReturn(Mono.just(existing));
|
||||||
|
when(noticeRepository.save(any(SysNotice.class)))
|
||||||
|
.thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||||
|
|
||||||
|
Mono<SysNotice> result = noticeService.updateNotice(1L, update);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(n -> n.getNoticeTitle().equals("新标题"))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(noticeRepository).save(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateNotice_shouldReturnEmptyWhenNotFound() {
|
||||||
|
SysNotice update = createTestNotice(999L, "新标题");
|
||||||
|
when(noticeRepository.findById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<SysNotice> result = noticeService.updateNotice(999L, update);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(noticeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== deleteNotice ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteNotice_shouldSoftDelete() {
|
||||||
|
SysNotice existing = createTestNotice(1L, "公告1");
|
||||||
|
when(noticeRepository.findById(1L)).thenReturn(Mono.just(existing));
|
||||||
|
when(noticeRepository.save(any(SysNotice.class))).thenReturn(Mono.just(existing));
|
||||||
|
|
||||||
|
Mono<Void> result = noticeService.deleteNotice(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(noticeRepository).save(existing);
|
||||||
|
assertThat(existing.getDeletedAt()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteNotice_shouldCompleteEmptyWhenNotFound() {
|
||||||
|
when(noticeRepository.findById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<Void> result = noticeService.deleteNotice(999L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(noticeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== helper ====================
|
||||||
|
|
||||||
|
private SysNotice createTestNotice(Long id, String title) {
|
||||||
|
SysNotice notice = new SysNotice();
|
||||||
|
notice.setId(id);
|
||||||
|
notice.setNoticeTitle(title);
|
||||||
|
notice.setNoticeContent("公告内容");
|
||||||
|
notice.setNoticeType("1");
|
||||||
|
notice.setStatus("1");
|
||||||
|
return notice;
|
||||||
|
}
|
||||||
|
}
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.core.service.impl;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.SysUserMessage;
|
||||||
|
import cn.novalon.gym.manage.notify.core.repository.ISysUserMessageRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SysUserMessageServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ISysUserMessageRepository messageRepository;
|
||||||
|
|
||||||
|
private SysUserMessageServiceImpl messageService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
messageService = new SysUserMessageServiceImpl(messageRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getMessagesByUser ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMessagesByUser_shouldReturnMessages() {
|
||||||
|
List<SysUserMessage> messages = List.of(createTestMessage(1L, 1L, "消息1"), createTestMessage(2L, 1L, "消息2"));
|
||||||
|
when(messageRepository.findByUserIdOrderByCreateTimeDesc(1L)).thenReturn(Flux.fromIterable(messages));
|
||||||
|
|
||||||
|
Flux<SysUserMessage> result = messageService.getMessagesByUser(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextCount(2)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMessagesByUser_shouldReturnEmptyWhenNone() {
|
||||||
|
when(messageRepository.findByUserIdOrderByCreateTimeDesc(999L)).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
Flux<SysUserMessage> result = messageService.getMessagesByUser(999L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getUnreadCount ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUnreadCount_shouldReturnCount() {
|
||||||
|
when(messageRepository.countByUserIdAndIsRead(1L, "0")).thenReturn(Mono.just(5L));
|
||||||
|
|
||||||
|
Mono<Long> result = messageService.getUnreadCount(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNext(5L)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUnreadCount_shouldReturnZero() {
|
||||||
|
when(messageRepository.countByUserIdAndIsRead(1L, "0")).thenReturn(Mono.just(0L));
|
||||||
|
|
||||||
|
Mono<Long> result = messageService.getUnreadCount(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNext(0L)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getUnreadMessages ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUnreadMessages_shouldReturnUnreadOnly() {
|
||||||
|
when(messageRepository.findByUserIdAndIsReadOrderByCreateTimeDesc(1L, "0"))
|
||||||
|
.thenReturn(Flux.just(createTestMessage(1L, 1L, "未读消息")));
|
||||||
|
|
||||||
|
Flux<SysUserMessage> result = messageService.getUnreadMessages(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextCount(1)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== createMessage ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createMessage_shouldSaveAndReturn() {
|
||||||
|
SysUserMessage message = createTestMessage(null, 1L, "新消息");
|
||||||
|
when(messageRepository.save(any(SysUserMessage.class)))
|
||||||
|
.thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||||
|
|
||||||
|
Mono<SysUserMessage> result = messageService.createMessage(message);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(m -> m.getTitle().equals("新消息"))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(messageRepository).save(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== markAsRead ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markAsRead_shouldUpdateAndReturnMessage() {
|
||||||
|
SysUserMessage existing = createTestMessage(1L, 1L, "消息");
|
||||||
|
when(messageRepository.findById(1L)).thenReturn(Mono.just(existing));
|
||||||
|
when(messageRepository.save(any(SysUserMessage.class))).thenReturn(Mono.just(existing));
|
||||||
|
|
||||||
|
Mono<SysUserMessage> result = messageService.markAsRead(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(m -> m.getIsRead().equals("1"))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(messageRepository).save(existing);
|
||||||
|
assertThat(existing.getIsRead()).isEqualTo("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markAsRead_shouldReturnEmptyWhenNotFound() {
|
||||||
|
when(messageRepository.findById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<SysUserMessage> result = messageService.markAsRead(999L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(messageRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== markAllAsRead ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markAllAsRead_shouldReturnCount() {
|
||||||
|
when(messageRepository.markAllAsReadByUserId(1L)).thenReturn(Mono.just(2L));
|
||||||
|
|
||||||
|
Mono<Long> result = messageService.markAllAsRead(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNext(2L)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(messageRepository).markAllAsReadByUserId(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markAllAsRead_shouldReturnZeroWhenNoUnread() {
|
||||||
|
when(messageRepository.markAllAsReadByUserId(1L)).thenReturn(Mono.just(0L));
|
||||||
|
|
||||||
|
Mono<Long> result = messageService.markAllAsRead(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNext(0L)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== deleteMessage ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteMessage_shouldDelete() {
|
||||||
|
when(messageRepository.deleteById(1L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<Void> result = messageService.deleteMessage(1L);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(messageRepository).deleteById(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== helper ====================
|
||||||
|
|
||||||
|
private SysUserMessage createTestMessage(Long id, Long userId, String title) {
|
||||||
|
SysUserMessage message = new SysUserMessage();
|
||||||
|
message.setId(id);
|
||||||
|
message.setUserId(userId);
|
||||||
|
message.setTitle(title);
|
||||||
|
message.setContent("测试内容");
|
||||||
|
message.setIsRead("0");
|
||||||
|
message.setCreateTime(LocalDateTime.now());
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||||
|
import cn.novalon.gym.manage.notify.core.service.IBannerService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class BannerHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IBannerService bannerService;
|
||||||
|
|
||||||
|
private BannerHandler bannerHandler;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
bannerHandler = new BannerHandler(bannerService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getAllBanners ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllBanners_shouldReturnOkWithBanners() {
|
||||||
|
List<Banner> banners = List.of(createTestBanner(1L, "轮播图1"), createTestBanner(2L, "轮播图2"));
|
||||||
|
when(bannerService.getAllBanners()).thenReturn(Flux.fromIterable(banners));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.getAllBanners(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllBanners_shouldReturnOkWhenEmpty() {
|
||||||
|
when(bannerService.getAllBanners()).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.getAllBanners(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getActiveBanners ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getActiveBanners_shouldReturnOkWithActiveBanners() {
|
||||||
|
when(bannerService.getActiveBanners()).thenReturn(Flux.just(createTestBanner(1L, "活跃轮播图")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder().build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.getActiveBanners(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getBannerById ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBannerById_shouldReturnOkWhenFound() {
|
||||||
|
when(bannerService.getBannerById(1L)).thenReturn(Mono.just(createTestBanner(1L, "轮播图1")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.getBannerById(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBannerById_shouldReturnNotFound() {
|
||||||
|
when(bannerService.getBannerById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.getBannerById(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== createBanner ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createBanner_shouldReturnOkWhenCreated() {
|
||||||
|
Map<String, Object> body = Map.of(
|
||||||
|
"imageUrl", "https://example.com/banner.jpg",
|
||||||
|
"title", "新轮播图",
|
||||||
|
"subtitle", "副标题",
|
||||||
|
"sortOrder", 1,
|
||||||
|
"isActive", true
|
||||||
|
);
|
||||||
|
when(bannerService.createBanner(any(Banner.class))).thenReturn(Mono.just(createTestBanner(1L, "新轮播图")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(body));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.createBanner(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== updateBanner ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateBanner_shouldReturnOkWhenUpdated() {
|
||||||
|
Map<String, Object> body = Map.of("title", "更新标题");
|
||||||
|
when(bannerService.updateBanner(eq(1L), any(Banner.class))).thenReturn(Mono.just(createTestBanner(1L, "更新标题")));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.body(Mono.just(body));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.updateBanner(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateBanner_shouldReturnNotFound() {
|
||||||
|
Map<String, Object> body = Map.of("title", "更新标题");
|
||||||
|
when(bannerService.updateBanner(eq(999L), any(Banner.class))).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.body(Mono.just(body));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.updateBanner(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== deleteBanner ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteBanner_shouldReturnNoContent() {
|
||||||
|
Banner banner = createTestBanner(1L, "轮播图1");
|
||||||
|
banner.setDeletedAt(null);
|
||||||
|
when(bannerService.getBannerById(1L)).thenReturn(Mono.just(banner));
|
||||||
|
when(bannerService.deleteBanner(1L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.deleteBanner(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteBanner_shouldReturnNotFound() {
|
||||||
|
when(bannerService.getBannerById(999L)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = bannerHandler.deleteBanner(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== helper ====================
|
||||||
|
|
||||||
|
private Banner createTestBanner(Long id, String title) {
|
||||||
|
Banner banner = new Banner();
|
||||||
|
banner.setId(id);
|
||||||
|
banner.setImageUrl("https://example.com/banner-" + id + ".jpg");
|
||||||
|
banner.setTitle(title);
|
||||||
|
banner.setSubtitle("副标题");
|
||||||
|
banner.setSortOrder(id.intValue());
|
||||||
|
banner.setIsActive("1");
|
||||||
|
banner.setCreatedAt(LocalDateTime.now());
|
||||||
|
return banner;
|
||||||
|
}
|
||||||
|
}
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.SysUserMessage;
|
||||||
|
import cn.novalon.gym.manage.notify.core.service.ISysUserMessageService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SysUserMessageHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ISysUserMessageService messageService;
|
||||||
|
|
||||||
|
private SysUserMessageHandler handler;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
handler = new SysUserMessageHandler(messageService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getMessagesByUser ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMessagesByUser_shouldReturnOkWithMessages() {
|
||||||
|
List<SysUserMessage> messages = List.of(createTestMessage(1L, "消息1"), createTestMessage(2L, "消息2"));
|
||||||
|
when(messageService.getMessagesByUser(anyLong())).thenReturn(Flux.fromIterable(messages));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("userId", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getMessagesByUser(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMessagesByUser_shouldReturnEmptyListWhenNone() {
|
||||||
|
when(messageService.getMessagesByUser(anyLong())).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("userId", "999")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getMessagesByUser(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getUnreadCount ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUnreadCount_shouldReturnOkWithCount() {
|
||||||
|
when(messageService.getUnreadCount(anyLong())).thenReturn(Mono.just(5L));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("userId", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getUnreadCount(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUnreadCount_shouldReturnZero() {
|
||||||
|
when(messageService.getUnreadCount(anyLong())).thenReturn(Mono.just(0L));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("userId", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getUnreadCount(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== getUnreadList ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUnreadList_shouldReturnOk() {
|
||||||
|
when(messageService.getUnreadMessages(anyLong())).thenReturn(Flux.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("userId", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.getUnreadList(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== createMessage ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createMessage_shouldReturnCreated() {
|
||||||
|
SysUserMessage message = createTestMessage(1L, "新消息");
|
||||||
|
when(messageService.createMessage(any(SysUserMessage.class))).thenReturn(Mono.just(message));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(message));
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.createMessage(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== markAsRead ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markAsRead_shouldReturnOk() {
|
||||||
|
SysUserMessage message = createTestMessage(1L, "消息");
|
||||||
|
when(messageService.markAsRead(anyLong())).thenReturn(Mono.just(message));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.markAsRead(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markAsRead_shouldReturnNotFound() {
|
||||||
|
when(messageService.markAsRead(anyLong())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.markAsRead(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== markAllAsRead ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markAllAsRead_shouldReturnOk() {
|
||||||
|
when(messageService.markAllAsRead(anyLong())).thenReturn(Mono.just(3L));
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("userId", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.markAllAsRead(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== deleteMessage ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteMessage_shouldReturnOk() {
|
||||||
|
when(messageService.deleteMessage(anyLong())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.deleteMessage(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteMessage_shouldReturnNotFound() {
|
||||||
|
when(messageService.deleteMessage(anyLong())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.pathVariable("id", "999")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<ServerResponse> result = handler.deleteMessage(request);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== helper ====================
|
||||||
|
|
||||||
|
private SysUserMessage createTestMessage(Long id, String title) {
|
||||||
|
SysUserMessage message = new SysUserMessage();
|
||||||
|
message.setId(id);
|
||||||
|
message.setUserId(1L);
|
||||||
|
message.setTitle(title);
|
||||||
|
message.setContent("测试内容-" + id);
|
||||||
|
message.setIsRead("0");
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -125,6 +125,52 @@ async function getCourseBookings(courseId, token) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 品牌定制 API ──
|
||||||
|
|
||||||
|
/** 获取品牌配置 */
|
||||||
|
async function getBrandConfig(token) {
|
||||||
|
console.log('[API] 获取品牌配置...');
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/brand`, null, token);
|
||||||
|
console.log(`[API] 品牌配置: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 违规记录 API ──
|
||||||
|
|
||||||
|
/** 获取所有教练违规统计 */
|
||||||
|
async function getViolationCounts(token) {
|
||||||
|
console.log('[API] 获取违规统计...');
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/coach/violation-counts`, null, token);
|
||||||
|
console.log(`[API] 违规统计: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取指定教练的违规记录 */
|
||||||
|
async function getCoachViolations(coachId, token) {
|
||||||
|
console.log(`[API] 获取教练违规: coachId=${coachId}`);
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/coach/${coachId}/violations`, null, token);
|
||||||
|
console.log(`[API] 违规记录: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 通知/消息 API ──
|
||||||
|
|
||||||
|
/** 获取活跃Banner列表 */
|
||||||
|
async function getActiveBanners(token) {
|
||||||
|
console.log('[API] 获取活跃Banners...');
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/banners/active`, null, token);
|
||||||
|
console.log(`[API] Banners: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取系统通知列表 */
|
||||||
|
async function getSystemNotices(token) {
|
||||||
|
console.log('[API] 获取系统通知...');
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/notices`, null, token);
|
||||||
|
console.log(`[API] 通知列表: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
makeRequest,
|
makeRequest,
|
||||||
coachLogin,
|
coachLogin,
|
||||||
@@ -133,6 +179,11 @@ module.exports = {
|
|||||||
startCourse,
|
startCourse,
|
||||||
endCourse,
|
endCourse,
|
||||||
getCourseBookings,
|
getCourseBookings,
|
||||||
|
getBrandConfig,
|
||||||
|
getViolationCounts,
|
||||||
|
getCoachViolations,
|
||||||
|
getActiveBanners,
|
||||||
|
getSystemNotices,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
API_BASE
|
API_BASE
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* P3-API - 教练端品牌定制 & 通知消息 & 违规记录 API 测试
|
||||||
|
* 流程:教练登录 → 获取品牌配置 → 获取Banner/通知 → 获取违规记录
|
||||||
|
*
|
||||||
|
* 使用 HTTP API(HMAC-SHA256 签名 + JWT Token)
|
||||||
|
* 前置条件:后端服务 http://192.168.110.64:8084 已启动
|
||||||
|
*/
|
||||||
|
const api = require('../helpers/api-helper');
|
||||||
|
|
||||||
|
const COACH_USERNAME = 'coach_zhang';
|
||||||
|
const COACH_PASSWORD = 'Test@123';
|
||||||
|
|
||||||
|
let authToken = null;
|
||||||
|
let coachId = null;
|
||||||
|
|
||||||
|
describe('P3-API - 教练端品牌定制 & 通知 & 违规记录', () => {
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤0:教练登录
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
beforeAll(async () => {
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log('P3-API - 教练端品牌定制 & 通知 & 违规记录');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
|
||||||
|
const result = await api.coachLogin(COACH_USERNAME, COACH_PASSWORD);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.token).toBeDefined();
|
||||||
|
authToken = result.token;
|
||||||
|
coachId = result.userId;
|
||||||
|
console.log(`[P3-API] 教练登录成功: userId=${coachId}`);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤1:品牌定制配置
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('品牌定制配置', () => {
|
||||||
|
test('TC-COACH-BRAND-001: 获取品牌配置', async () => {
|
||||||
|
const res = await api.getBrandConfig(authToken);
|
||||||
|
console.log(`[TC-COACH-BRAND-001] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const brand = res.data?.data || res.data || {};
|
||||||
|
console.log(` 品牌名称: ${brand.brandName || '(未设置)'}`);
|
||||||
|
console.log(` 品牌口号: ${brand.slogan || '(未设置)'}`);
|
||||||
|
console.log(` 主色调: ${brand.primaryColor || '(默认)'}`);
|
||||||
|
console.log(` 辅助色: ${brand.secondaryColor || '(默认)'}`);
|
||||||
|
|
||||||
|
expect(brand).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-COACH-BRAND-002: 品牌颜色为合法HEX值', async () => {
|
||||||
|
const res = await api.getBrandConfig(authToken);
|
||||||
|
if (!api.isSuccess(res)) {
|
||||||
|
console.warn('[TC-COACH-BRAND-002] 获取失败,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const brand = res.data?.data || res.data || {};
|
||||||
|
if (brand.primaryColor) {
|
||||||
|
expect(/^#[0-9A-Fa-f]{6,8}$/.test(brand.primaryColor)).toBe(true);
|
||||||
|
console.log(` 主色调 HEX 合法: ${brand.primaryColor}`);
|
||||||
|
}
|
||||||
|
if (brand.secondaryColor) {
|
||||||
|
expect(/^#[0-9A-Fa-f]{6,8}$/.test(brand.secondaryColor)).toBe(true);
|
||||||
|
console.log(` 辅助色 HEX 合法: ${brand.secondaryColor}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤2:违规记录
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('违规记录', () => {
|
||||||
|
test('TC-VIOLATION-001: 获取所有教练违规统计', async () => {
|
||||||
|
const res = await api.getViolationCounts(authToken);
|
||||||
|
console.log(`[TC-VIOLATION-001] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const counts = res.data?.data || res.data || [];
|
||||||
|
console.log(` 违规统计条目数: ${Array.isArray(counts) ? counts.length : 'N/A'}`);
|
||||||
|
|
||||||
|
if (Array.isArray(counts) && counts.length > 0) {
|
||||||
|
counts.slice(0, 3).forEach(c => {
|
||||||
|
console.log(` - coachName=${c.coachName || c.name}, violations=${c.violationCount || c.count}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-VIOLATION-002: 获取当前教练违规记录', async () => {
|
||||||
|
const res = await api.getCoachViolations(coachId, authToken);
|
||||||
|
console.log(`[TC-VIOLATION-002] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const violations = res.data?.data || res.data || [];
|
||||||
|
const records = Array.isArray(violations) ? violations : (violations.content || violations.records || []);
|
||||||
|
console.log(` 教练 ${coachId} 违规记录数: ${records.length}`);
|
||||||
|
|
||||||
|
if (records.length > 0) {
|
||||||
|
records.slice(0, 3).forEach(v => {
|
||||||
|
console.log(` - type=${v.type || v.violationType}, time=${v.time || v.createTime}, desc=${(v.description || '').substring(0, 40)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-VIOLATION-003: 不存在教练ID应返回空', async () => {
|
||||||
|
const res = await api.getCoachViolations(99999, authToken);
|
||||||
|
console.log(`[TC-VIOLATION-003] 不存在教练: status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const violations = res.data?.data || res.data || [];
|
||||||
|
const records = Array.isArray(violations) ? violations : [];
|
||||||
|
expect(records.length).toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤3:Banner & 系统通知
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('Banner & 系统通知', () => {
|
||||||
|
test('TC-COACH-NOTIFY-001: 获取活跃Banner', async () => {
|
||||||
|
const res = await api.getActiveBanners(authToken);
|
||||||
|
console.log(`[TC-COACH-NOTIFY-001] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const banners = res.data?.data || res.data || [];
|
||||||
|
const records = Array.isArray(banners) ? banners : (banners.records || []);
|
||||||
|
console.log(` 活跃Banner数量: ${records.length}`);
|
||||||
|
expect(records.length).toBeGreaterThanOrEqual(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-COACH-NOTIFY-002: 获取系统通知', async () => {
|
||||||
|
const res = await api.getSystemNotices(authToken);
|
||||||
|
console.log(`[TC-COACH-NOTIFY-002] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const notices = res.data?.data || res.data || [];
|
||||||
|
const records = Array.isArray(notices) ? notices : (notices.records || []);
|
||||||
|
console.log(` 系统通知数量: ${records.length}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤4:总结
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('总结', () => {
|
||||||
|
test('TC-P3-COACH-SUMMARY: 教练端品牌/通知/违规测试汇总', () => {
|
||||||
|
console.log('');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log('P3-API - 教练端品牌 & 通知 & 违规记录测试结束');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log(' 已测试:');
|
||||||
|
console.log(' 1. 品牌配置 (名称/颜色)');
|
||||||
|
console.log(' 2. 所有教练违规统计');
|
||||||
|
console.log(' 3. 当前教练违规记录');
|
||||||
|
console.log(' 4. 不存在教练违规边界');
|
||||||
|
console.log(' 5. 活跃Banner列表');
|
||||||
|
console.log(' 6. 系统通知列表');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
|
||||||
|
expect(coachId).toBeDefined();
|
||||||
|
expect(authToken).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -173,6 +173,90 @@ async function signIn(courseId, memberId, token) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 品牌定制 API ──
|
||||||
|
|
||||||
|
/** 获取品牌配置 (需auth) */
|
||||||
|
async function getBrandConfig(token) {
|
||||||
|
console.log('[API] 获取品牌配置...');
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/brand`, null, token);
|
||||||
|
console.log(`[API] 品牌配置: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 通知/消息 API ──
|
||||||
|
|
||||||
|
/** 获取活跃Banner列表 */
|
||||||
|
async function getActiveBanners(token) {
|
||||||
|
console.log('[API] 获取活跃Banners...');
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/banners/active`, null, token);
|
||||||
|
console.log(`[API] Banners: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取系统通知列表 */
|
||||||
|
async function getSystemNotices(token) {
|
||||||
|
console.log('[API] 获取系统通知...');
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/notices`, null, token);
|
||||||
|
console.log(`[API] 通知列表: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取用户消息列表 */
|
||||||
|
async function getUserMessages(userId, token) {
|
||||||
|
console.log(`[API] 获取用户消息: userId=${userId}`);
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/messages/user/${userId}`, null, token);
|
||||||
|
console.log(`[API] 用户消息: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取用户未读消息数 */
|
||||||
|
async function getUnreadMessageCount(userId, token) {
|
||||||
|
console.log(`[API] 获取未读消息数: userId=${userId}`);
|
||||||
|
const res = await makeRequest('GET', `${API_BASE}/messages/user/${userId}/unread`, null, token);
|
||||||
|
console.log(`[API] 未读数: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记单条消息已读 */
|
||||||
|
async function markMessageAsRead(messageId, token) {
|
||||||
|
console.log(`[API] 标记消息已读: id=${messageId}`);
|
||||||
|
const res = await makeRequest('PUT', `${API_BASE}/messages/${messageId}/read`, null, token);
|
||||||
|
console.log(`[API] 标记已读: status=${res.status}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 团课高级搜索 API ──
|
||||||
|
|
||||||
|
/** 按课程类型筛选 */
|
||||||
|
async function searchCoursesByType(typeId, token) {
|
||||||
|
console.log(`[API] 按类型搜索: typeId=${typeId}`);
|
||||||
|
const res = await makeRequest('POST', `${API_BASE}/groupCourse/page`, {
|
||||||
|
page: 0, size: 50, courseTypeId: typeId
|
||||||
|
}, token);
|
||||||
|
console.log(`[API] 类型筛选: status=${res.status}, total=${res.data?.data?.totalElements || '?'}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按标签筛选 */
|
||||||
|
async function searchCoursesByLabel(labelId, token) {
|
||||||
|
console.log(`[API] 按标签搜索: labelId=${labelId}`);
|
||||||
|
const res = await makeRequest('POST', `${API_BASE}/groupCourse/page`, {
|
||||||
|
page: 0, size: 50, labelId: labelId
|
||||||
|
}, token);
|
||||||
|
console.log(`[API] 标签筛选: status=${res.status}, total=${res.data?.data?.totalElements || '?'}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 复筛: 类型+标签 */
|
||||||
|
async function searchCoursesCombined(typeId, labelId, token) {
|
||||||
|
console.log(`[API] 组合搜索: typeId=${typeId}, labelId=${labelId}`);
|
||||||
|
const res = await makeRequest('POST', `${API_BASE}/groupCourse/page`, {
|
||||||
|
page: 0, size: 50, courseTypeId: typeId, labelId: labelId
|
||||||
|
}, token);
|
||||||
|
console.log(`[API] 组合筛选: status=${res.status}, total=${res.data?.data?.totalElements || '?'}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
makeRequest,
|
makeRequest,
|
||||||
adminLogin,
|
adminLogin,
|
||||||
@@ -185,6 +269,15 @@ module.exports = {
|
|||||||
bookCourse,
|
bookCourse,
|
||||||
getMemberBookings,
|
getMemberBookings,
|
||||||
signIn,
|
signIn,
|
||||||
|
getBrandConfig,
|
||||||
|
getActiveBanners,
|
||||||
|
getSystemNotices,
|
||||||
|
getUserMessages,
|
||||||
|
getUnreadMessageCount,
|
||||||
|
markMessageAsRead,
|
||||||
|
searchCoursesByType,
|
||||||
|
searchCoursesByLabel,
|
||||||
|
searchCoursesCombined,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
API_BASE
|
API_BASE
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* P4-API - 会员端团课高级搜索 API 测试
|
||||||
|
* 流程:会员登录 → 按课程类型筛选 → 按标签筛选 → 类型+标签组合筛选 → 验证结果
|
||||||
|
*
|
||||||
|
* 使用 HTTP API(HMAC-SHA256 签名 + JWT Token)
|
||||||
|
* 前置条件:后端服务 http://192.168.110.64:8084 已启动
|
||||||
|
*/
|
||||||
|
const api = require('../helpers/api-helper');
|
||||||
|
|
||||||
|
let authToken = null;
|
||||||
|
let memberId = null;
|
||||||
|
let typeId = null;
|
||||||
|
let labelId = null;
|
||||||
|
|
||||||
|
describe('P4-API - 团课高级搜索', () => {
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤0:登录 & 准备筛选条件
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
beforeAll(async () => {
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log('P4-API - 团课高级搜索测试');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
|
||||||
|
const result = await api.memberLogin();
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
authToken = result.accessToken;
|
||||||
|
memberId = result.memberId;
|
||||||
|
console.log(`[P4-API] 会员登录成功: memberId=${memberId}`);
|
||||||
|
|
||||||
|
// 先获取课程类型列表,找第一个类型ID
|
||||||
|
const typeRes = await api.getCourseTypes();
|
||||||
|
if (api.isSuccess(typeRes)) {
|
||||||
|
const types = typeRes.data?.data || typeRes.data || [];
|
||||||
|
const records = Array.isArray(types) ? types : (types.records || types.content || []);
|
||||||
|
if (records.length > 0) {
|
||||||
|
typeId = records[0].id;
|
||||||
|
console.log(`[P4-API] 选择课程类型: id=${typeId}, name=${records[0].typeName || records[0].name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从课程列表获取第一个标签ID
|
||||||
|
try {
|
||||||
|
const searchRes = await api.searchCourses('', authToken);
|
||||||
|
if (api.isSuccess(searchRes)) {
|
||||||
|
const content = searchRes.data?.data?.content || searchRes.data?.content || searchRes.data || [];
|
||||||
|
const courses = Array.isArray(content) ? content : (content.records || []);
|
||||||
|
// 从第一个课程取标签
|
||||||
|
for (const course of courses) {
|
||||||
|
const labels = course.labels || course.labelList || [];
|
||||||
|
if (Array.isArray(labels) && labels.length > 0) {
|
||||||
|
const firstLabel = labels[0];
|
||||||
|
labelId = typeof firstLabel === 'object' ? firstLabel.id : firstLabel;
|
||||||
|
const name = typeof firstLabel === 'object' ? firstLabel.labelName : '';
|
||||||
|
console.log(`[P4-API] 选择标签: id=${labelId}, name=${name}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[P4-API] 获取标签失败:', e.message);
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤1:按课程类型筛选
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('按课程类型筛选', () => {
|
||||||
|
test('TC-SEARCH-001: 按类型筛选应返回正确结果', async () => {
|
||||||
|
if (!typeId) {
|
||||||
|
console.warn('[TC-SEARCH-001] 无类型ID,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.searchCoursesByType(typeId, authToken);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const content = res.data?.data?.content || res.data?.content || res.data || [];
|
||||||
|
const courses = Array.isArray(content) ? content : (content.records || []);
|
||||||
|
console.log(`[TC-SEARCH-001] 类型筛选结果: ${courses.length} 个课程`);
|
||||||
|
|
||||||
|
// 验证所有返回课程的类型匹配
|
||||||
|
if (courses.length > 0) {
|
||||||
|
courses.slice(0, 3).forEach(c => {
|
||||||
|
console.log(` - ${c.courseName || c.name} (typeId=${c.courseTypeId || c.typeId})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(courses.length).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤2:按标签筛选
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('按标签筛选', () => {
|
||||||
|
test('TC-SEARCH-002: 按标签筛选应返回正确结果', async () => {
|
||||||
|
if (!labelId) {
|
||||||
|
console.warn('[TC-SEARCH-002] 无标签ID,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.searchCoursesByLabel(labelId, authToken);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const content = res.data?.data?.content || res.data?.content || res.data || [];
|
||||||
|
const courses = Array.isArray(content) ? content : (content.records || []);
|
||||||
|
console.log(`[TC-SEARCH-002] 标签筛选结果: ${courses.length} 个课程`);
|
||||||
|
|
||||||
|
if (courses.length > 0) {
|
||||||
|
courses.slice(0, 3).forEach(c => {
|
||||||
|
const labels = (c.labels || c.labelList || []).map(l =>
|
||||||
|
typeof l === 'object' ? l.labelName : l
|
||||||
|
);
|
||||||
|
console.log(` - ${c.courseName || c.name} (labels: [${labels.join(', ')}])`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤3:类型+标签组合筛选
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('类型+标签组合筛选', () => {
|
||||||
|
test('TC-SEARCH-003: 组合筛选应返回交集结果', async () => {
|
||||||
|
if (!typeId || !labelId) {
|
||||||
|
console.warn('[TC-SEARCH-003] 缺少类型或标签ID,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.searchCoursesCombined(typeId, labelId, authToken);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const content = res.data?.data?.content || res.data?.content || res.data || [];
|
||||||
|
const courses = Array.isArray(content) ? content : (content.records || []);
|
||||||
|
console.log(`[TC-SEARCH-003] 组合筛选结果: ${courses.length} 个课程`);
|
||||||
|
|
||||||
|
if (courses.length > 0) {
|
||||||
|
courses.slice(0, 3).forEach(c => {
|
||||||
|
console.log(` - ${c.courseName || c.name} (id=${c.id})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤4:边界场景
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('边界场景', () => {
|
||||||
|
test('TC-SEARCH-004: 搜索不存在类型返回200(后端可能忽略无效筛选)', async () => {
|
||||||
|
const res = await api.searchCoursesByType(99999, authToken);
|
||||||
|
// 应确保API正常响应,即使筛选参数无效也不应该崩溃
|
||||||
|
console.log(`[TC-SEARCH-004] 不存在类型: status=${res.status}`);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-SEARCH-005: 搜索不存在标签返回200(后端可能忽略无效筛选)', async () => {
|
||||||
|
const res = await api.searchCoursesByLabel(99999, authToken);
|
||||||
|
console.log(`[TC-SEARCH-005] 不存在标签: status=${res.status}`);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-SEARCH-006: 空关键词搜索可返回全部课程', async () => {
|
||||||
|
const res = await api.searchCourses('', authToken);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const content = res.data?.data?.content || res.data?.content || res.data || [];
|
||||||
|
const courses = Array.isArray(content) ? content : (content.records || []);
|
||||||
|
console.log(`[TC-SEARCH-006] 空搜索返回: ${courses.length} 个课程`);
|
||||||
|
expect(courses.length).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤5:总结
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('总结', () => {
|
||||||
|
test('TC-P4-SUMMARY: 团课高级搜索汇总', () => {
|
||||||
|
console.log('');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log('P4-API - 团课高级搜索测试结束');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log(' 已测试:');
|
||||||
|
console.log(` 1. 按类型筛选 (typeId=${typeId || 'N/A'})`);
|
||||||
|
console.log(` 2. 按标签筛选 (labelId=${labelId || 'N/A'})`);
|
||||||
|
console.log(` 3. 类型+标签组合筛选`);
|
||||||
|
console.log(' 4. 不存在类型/标签边界');
|
||||||
|
console.log(' 5. 空关键词搜索');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
|
||||||
|
expect(memberId).toBeDefined();
|
||||||
|
expect(authToken).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* P3-API - 会员端品牌定制 & 通知消息 API 测试
|
||||||
|
* 流程:会员登录 → 获取品牌配置 → 获取Banner/通知
|
||||||
|
*
|
||||||
|
* 使用 HTTP API(HMAC-SHA256 签名 + JWT Token)
|
||||||
|
* 前置条件:后端服务 http://192.168.110.64:8084 已启动
|
||||||
|
*/
|
||||||
|
const api = require('../helpers/api-helper');
|
||||||
|
|
||||||
|
let authToken = null;
|
||||||
|
let memberId = null;
|
||||||
|
|
||||||
|
describe('P3-API - 会员端品牌定制 & 通知消息', () => {
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤0:会员登录
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
beforeAll(async () => {
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log('P3-API - 品牌定制 & 通知消息测试');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
|
||||||
|
const result = await api.memberLogin();
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
authToken = result.accessToken;
|
||||||
|
memberId = result.memberId;
|
||||||
|
console.log(`[P3-API] 会员登录成功: memberId=${memberId}`);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤1:品牌定制配置
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('品牌定制配置', () => {
|
||||||
|
test('TC-BRAND-001: 获取品牌配置', async () => {
|
||||||
|
const res = await api.getBrandConfig(authToken);
|
||||||
|
console.log(`[TC-BRAND-001] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const brand = res.data?.data || res.data || {};
|
||||||
|
console.log(` 品牌名称: ${brand.brandName || '(未设置)'}`);
|
||||||
|
console.log(` 品牌口号: ${brand.slogan || '(未设置)'}`);
|
||||||
|
console.log(` 主色调: ${brand.primaryColor || '(默认)'}`);
|
||||||
|
console.log(` 辅助色: ${brand.secondaryColor || '(默认)'}`);
|
||||||
|
console.log(` Logo URL: ${brand.logoUrl || '(默认)'}`);
|
||||||
|
console.log(` 背景URL: ${brand.backgroundUrl || '(默认)'}`);
|
||||||
|
|
||||||
|
// 品牌名称应存在(至少默认值)
|
||||||
|
expect(brand).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-BRAND-002: 验证品牌颜色为合法HEX值', async () => {
|
||||||
|
const res = await api.getBrandConfig(authToken);
|
||||||
|
if (!api.isSuccess(res)) {
|
||||||
|
console.warn('[TC-BRAND-002] 获取品牌配置失败,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const brand = res.data?.data || res.data || {};
|
||||||
|
const primaryColor = brand.primaryColor;
|
||||||
|
const secondaryColor = brand.secondaryColor;
|
||||||
|
|
||||||
|
if (primaryColor) {
|
||||||
|
// 验证是合法 HEX (#RRGGBB 或 #RRGGBBAA)
|
||||||
|
expect(/^#[0-9A-Fa-f]{6,8}$/.test(primaryColor)).toBe(true);
|
||||||
|
console.log(` 主色调 HEX 合法: ${primaryColor}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secondaryColor) {
|
||||||
|
expect(/^#[0-9A-Fa-f]{6,8}$/.test(secondaryColor)).toBe(true);
|
||||||
|
console.log(` 辅助色 HEX 合法: ${secondaryColor}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-BRAND-003: Logo URL 应为有效路径或默认占位符', async () => {
|
||||||
|
const res = await api.getBrandConfig(authToken);
|
||||||
|
if (!api.isSuccess(res)) {
|
||||||
|
console.warn('[TC-BRAND-003] 获取品牌配置失败,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const brand = res.data?.data || res.data || {};
|
||||||
|
const logoUrl = brand.logoUrl;
|
||||||
|
|
||||||
|
if (logoUrl) {
|
||||||
|
// 可以是完整URL、相对路径、或base64
|
||||||
|
const isValid = typeof logoUrl === 'string' && logoUrl.length > 0;
|
||||||
|
expect(isValid).toBe(true);
|
||||||
|
console.log(` Logo URL: ${logoUrl.substring(0, 80)}...`);
|
||||||
|
} else {
|
||||||
|
console.log(' Logo URL 为空(使用默认值)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤2:Banner & 系统通知
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('Banner & 系统通知', () => {
|
||||||
|
test('TC-NOTIFY-001: 获取活跃Banner列表', async () => {
|
||||||
|
const res = await api.getActiveBanners(authToken);
|
||||||
|
console.log(`[TC-NOTIFY-001] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const banners = res.data?.data || res.data || [];
|
||||||
|
const records = Array.isArray(banners) ? banners : (banners.records || []);
|
||||||
|
console.log(` 活跃Banner数量: ${records.length}`);
|
||||||
|
|
||||||
|
if (records.length > 0) {
|
||||||
|
const b = records[0];
|
||||||
|
console.log(` 首条Banner: title="${b.title}", imageUrl=${(b.imageUrl || '').substring(0, 50)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-NOTIFY-002: 获取系统通知列表', async () => {
|
||||||
|
const res = await api.getSystemNotices(authToken);
|
||||||
|
console.log(`[TC-NOTIFY-002] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const notices = res.data?.data || res.data || [];
|
||||||
|
const records = Array.isArray(notices) ? notices : (notices.records || []);
|
||||||
|
console.log(` 系统通知数量: ${records.length}`);
|
||||||
|
|
||||||
|
if (records.length > 0) {
|
||||||
|
const n = records[0];
|
||||||
|
console.log(` 首条通知: title="${n.noticeTitle || n.title}", status=${n.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-NOTIFY-003: 获取用户消息列表', async () => {
|
||||||
|
const res = await api.getUserMessages(memberId, authToken);
|
||||||
|
console.log(`[TC-NOTIFY-003] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const messages = res.data?.data || res.data || [];
|
||||||
|
const records = Array.isArray(messages) ? messages : (messages.records || []);
|
||||||
|
console.log(` 消息数量: ${records.length}`);
|
||||||
|
}
|
||||||
|
// 消息可能为空,不强制要求有数据
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-NOTIFY-004: 获取未读消息数', async () => {
|
||||||
|
const res = await api.getUnreadMessageCount(memberId, authToken);
|
||||||
|
console.log(`[TC-NOTIFY-004] status=${res.status}`);
|
||||||
|
|
||||||
|
if (api.isSuccess(res)) {
|
||||||
|
const count = Number(res.data?.data ?? res.data ?? 0);
|
||||||
|
console.log(` 未读消息数: ${count}`);
|
||||||
|
expect(typeof count).toBe('number');
|
||||||
|
expect(count).toBeGreaterThanOrEqual(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// 步骤3:总结
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
describe('总结', () => {
|
||||||
|
test('TC-P3-SUMMARY: 品牌定制 & 通知消息测试汇总', () => {
|
||||||
|
console.log('');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log('P3-API - 品牌定制 & 通知消息测试结束');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
console.log(' 已测试:');
|
||||||
|
console.log(' 1. 品牌配置获取 (名称/颜色/Logo/背景)');
|
||||||
|
console.log(' 2. 颜色HEX格式校验');
|
||||||
|
console.log(' 3. Logo URL有效性');
|
||||||
|
console.log(' 4. 活跃Banner列表');
|
||||||
|
console.log(' 5. 系统通知列表');
|
||||||
|
console.log(' 6. 用户消息列表');
|
||||||
|
console.log(' 7. 未读消息数量');
|
||||||
|
console.log('════════════════════════════════════════════');
|
||||||
|
|
||||||
|
expect(memberId).toBeDefined();
|
||||||
|
expect(authToken).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('品牌定制管理完整工作流', () => {
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
|
const testBrandName = `测试品牌_${Date.now()}`;
|
||||||
|
const testSlogan = `测试口号_${Date.now()}`;
|
||||||
|
const testPrimaryColor = '#FF5722';
|
||||||
|
const testSecondaryColor = '#37474F';
|
||||||
|
|
||||||
|
test('品牌定制页面可正常加载', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制页面', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证页面标题存在', async () => {
|
||||||
|
await expect(page.locator('h2')).toContainText('品牌定制', { timeout: 10000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证四个配置卡片渲染', async () => {
|
||||||
|
// Logo 设置卡片
|
||||||
|
await expect(page.locator('.brand-card').filter({ hasText: 'Logo 设置' })).toBeVisible({ timeout: 5000 });
|
||||||
|
// 背景图设置卡片
|
||||||
|
await expect(page.locator('.brand-card').filter({ hasText: '背景图设置' })).toBeVisible({ timeout: 5000 });
|
||||||
|
// 品牌配色卡片
|
||||||
|
await expect(page.locator('.brand-card').filter({ hasText: '品牌配色' })).toBeVisible({ timeout: 5000 });
|
||||||
|
// 品牌信息卡片
|
||||||
|
await expect(page.locator('.brand-card').filter({ hasText: '品牌信息' })).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证实时预览区域存在', async () => {
|
||||||
|
await expect(page.locator('.brand-card').filter({ hasText: '实时预览' })).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证 WebSocket 连接标签存在', async () => {
|
||||||
|
const wsTag = page.locator('.brand-page-header .el-tag');
|
||||||
|
await expect(wsTag).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('更新品牌信息', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('填写品牌名称', async () => {
|
||||||
|
const brandCard = page.locator('.brand-card').filter({ hasText: '品牌信息' });
|
||||||
|
const nameInput = brandCard.locator('input').first();
|
||||||
|
await nameInput.clear();
|
||||||
|
await nameInput.fill(testBrandName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('填写品牌口号', async () => {
|
||||||
|
const brandCard = page.locator('.brand-card').filter({ hasText: '品牌信息' });
|
||||||
|
const sloganInput = brandCard.locator('input').nth(1);
|
||||||
|
await sloganInput.clear();
|
||||||
|
await sloganInput.fill(testSlogan);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('点击保存信息', async () => {
|
||||||
|
const brandCard = page.locator('.brand-card').filter({ hasText: '品牌信息' });
|
||||||
|
await brandCard.locator('button:has-text("保存信息")').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证保存成功提示', async () => {
|
||||||
|
await expect(page.locator('.el-message--success').first()).toBeVisible({ timeout: 8000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证预览区域品牌名称更新', async () => {
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const mockBrandName = page.locator('.mock-brand-name');
|
||||||
|
await expect(mockBrandName).toContainText(testBrandName, { timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('更新品牌配色', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('修改主色调', async () => {
|
||||||
|
const colorCard = page.locator('.brand-card').filter({ hasText: '品牌配色' });
|
||||||
|
// 主色调 HEX 输入框(.color-input 是 el-input 容器,需定位内部 <input>)
|
||||||
|
const primaryHexInput = colorCard.locator('.color-row').first().locator('input').first();
|
||||||
|
await primaryHexInput.clear();
|
||||||
|
await primaryHexInput.fill(testPrimaryColor);
|
||||||
|
// 触发 blur 验证
|
||||||
|
await primaryHexInput.blur();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('修改辅助色', async () => {
|
||||||
|
const colorCard = page.locator('.brand-card').filter({ hasText: '品牌配色' });
|
||||||
|
const secondaryHexInput = colorCard.locator('.color-row').nth(1).locator('input').first();
|
||||||
|
await secondaryHexInput.clear();
|
||||||
|
await secondaryHexInput.fill(testSecondaryColor);
|
||||||
|
await secondaryHexInput.blur();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('保存配色', async () => {
|
||||||
|
const colorCard = page.locator('.brand-card').filter({ hasText: '品牌配色' });
|
||||||
|
await colorCard.locator('button:has-text("保存配色")').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证保存成功', async () => {
|
||||||
|
await expect(page.locator('.el-message--success').first()).toBeVisible({ timeout: 8000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('品牌配色 - 输入非法HEX值应校验', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('输入无效HEX值并尝试保存', async () => {
|
||||||
|
const colorCard = page.locator('.brand-card').filter({ hasText: '品牌配色' });
|
||||||
|
const primaryHexInput = colorCard.locator('.color-row').first().locator('input').first();
|
||||||
|
await primaryHexInput.clear();
|
||||||
|
await primaryHexInput.fill('not-a-color');
|
||||||
|
await primaryHexInput.blur();
|
||||||
|
|
||||||
|
await colorCard.locator('button:has-text("保存配色")').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证校验警告提示', async () => {
|
||||||
|
await expect(page.locator('.el-message--warning').first()).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('品牌信息 - 空名称校验', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('清空品牌名称并保存', async () => {
|
||||||
|
const brandCard = page.locator('.brand-card').filter({ hasText: '品牌信息' });
|
||||||
|
const nameInput = brandCard.locator('input').first();
|
||||||
|
await nameInput.clear();
|
||||||
|
await brandCard.locator('button:has-text("保存信息")').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证警告提示', async () => {
|
||||||
|
await expect(page.locator('.el-message--warning').first()).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Logo 区域 - 上传组件正常渲染', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证 Logo 上传区域存在', async () => {
|
||||||
|
const logoCard = page.locator('.brand-card').filter({ hasText: 'Logo 设置' });
|
||||||
|
await expect(logoCard.locator('.el-upload')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('背景图区域 - 上传组件正常渲染', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证背景图上传区域存在', async () => {
|
||||||
|
const bgCard = page.locator('.brand-card').filter({ hasText: '背景图设置' });
|
||||||
|
await expect(bgCard.locator('.el-upload')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('实时预览 - 底部导航切换', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证默认首页可见', async () => {
|
||||||
|
await expect(page.locator('.mock-brand-section')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(page.locator('.mock-greeting-card')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('切换到课程tab', async () => {
|
||||||
|
await page.locator('.mock-tab-item').nth(1).click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expect(page.locator('.mock-search-area')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('切换到我的课程tab', async () => {
|
||||||
|
await page.locator('.mock-tab-item').nth(2).click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expect(page.locator('.mock-booking-card').first()).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('切换到我的tab', async () => {
|
||||||
|
await page.locator('.mock-tab-item').nth(3).click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expect(page.locator('.mock-profile-card')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('切换回首页', async () => {
|
||||||
|
await page.locator('.mock-tab-item').first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expect(page.locator('.mock-greeting-card')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('实时预览 - 课程详情钻取与返回', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('点击今日推荐课程', async () => {
|
||||||
|
await page.locator('.mock-course-card').first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证课程详情页渲染', async () => {
|
||||||
|
await expect(page.locator('.mock-detail-header')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(page.locator('.mock-detail-title')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证详情信息卡片', async () => {
|
||||||
|
await expect(page.locator('.mock-detail-info')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('点击返回按钮', async () => {
|
||||||
|
await page.locator('.mock-back-btn').click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证返回首页', async () => {
|
||||||
|
await expect(page.locator('.mock-greeting-card')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('实时预览 - 今日推荐查看更多进入搜索', async ({ page }) => {
|
||||||
|
await test.step('导航到品牌定制', async () => {
|
||||||
|
await page.goto('/brand');
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('点击查看全部', async () => {
|
||||||
|
await page.locator('.mock-section-more').click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('验证进入课程搜索页', async () => {
|
||||||
|
await expect(page.locator('.mock-search-area')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(page.locator('.mock-search-list')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
const { mockRequest } = vi.hoisted(() => ({
|
||||||
|
mockRequest: {
|
||||||
|
get: vi.fn().mockResolvedValue({}),
|
||||||
|
post: vi.fn().mockResolvedValue({}),
|
||||||
|
put: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/request', () => ({ default: mockRequest }))
|
||||||
|
|
||||||
|
import { bannerApi } from '@/api/banner.api'
|
||||||
|
|
||||||
|
describe('bannerApi', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should call GET /banners', async () => {
|
||||||
|
await bannerApi.getAll()
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/banners')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getActive', () => {
|
||||||
|
it('should call GET /banners/active', async () => {
|
||||||
|
await bannerApi.getActive()
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/banners/active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getById', () => {
|
||||||
|
it('should call GET /banners/:id', async () => {
|
||||||
|
await bannerApi.getById(1)
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/banners/1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should call POST /banners with data', async () => {
|
||||||
|
const data = { imageUrl: 'https://example.com/banner.jpg', title: 'New Banner' }
|
||||||
|
await bannerApi.create(data)
|
||||||
|
expect(mockRequest.post).toHaveBeenCalledWith('/banners', data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should call PUT /banners/:id with data', async () => {
|
||||||
|
const data = { imageUrl: 'https://example.com/banner.jpg', title: 'Updated' }
|
||||||
|
await bannerApi.update(1, data)
|
||||||
|
expect(mockRequest.put).toHaveBeenCalledWith('/banners/1', data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should call DELETE /banners/:id', async () => {
|
||||||
|
await bannerApi.delete(1)
|
||||||
|
expect(mockRequest.delete).toHaveBeenCalledWith('/banners/1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
const { mockRequest } = vi.hoisted(() => ({
|
||||||
|
mockRequest: {
|
||||||
|
get: vi.fn().mockResolvedValue({}),
|
||||||
|
post: vi.fn().mockResolvedValue({}),
|
||||||
|
put: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/request', () => ({ default: mockRequest }))
|
||||||
|
|
||||||
|
import { coachApi } from '@/api/coach.api'
|
||||||
|
|
||||||
|
describe('coachApi', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should call GET /coach/list', async () => {
|
||||||
|
await coachApi.getAll()
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/coach/list')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should call POST /coach with data', async () => {
|
||||||
|
const data = { username: 'coach1', password: 'pass', nickname: 'Coach One', email: 'c@test.com', phone: '123' }
|
||||||
|
await coachApi.create(data)
|
||||||
|
expect(mockRequest.post).toHaveBeenCalledWith('/coach', data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should call PUT /coach/:id with data', async () => {
|
||||||
|
const data = { nickname: 'New Name' }
|
||||||
|
await coachApi.update('1', data)
|
||||||
|
expect(mockRequest.put).toHaveBeenCalledWith('/coach/1', data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('disable', () => {
|
||||||
|
it('should call POST /coach/:id/disable', async () => {
|
||||||
|
await coachApi.disable('1')
|
||||||
|
expect(mockRequest.post).toHaveBeenCalledWith('/coach/1/disable')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getCourses', () => {
|
||||||
|
it('should call GET /coach/:id/courses', async () => {
|
||||||
|
await coachApi.getCourses('1')
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/coach/1/courses')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getViolationCounts', () => {
|
||||||
|
it('should call GET /coach/violation-counts', async () => {
|
||||||
|
await coachApi.getViolationCounts()
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/coach/violation-counts')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getViolations', () => {
|
||||||
|
it('should call GET /coach/:id/violations', async () => {
|
||||||
|
await coachApi.getViolations('1')
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/coach/1/violations')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
const { mockRequest } = vi.hoisted(() => ({
|
||||||
|
mockRequest: {
|
||||||
|
get: vi.fn().mockResolvedValue({}),
|
||||||
|
post: vi.fn().mockResolvedValue({}),
|
||||||
|
put: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/request', () => ({ default: mockRequest }))
|
||||||
|
|
||||||
|
import { memberCardApi } from '@/api/memberCard.api'
|
||||||
|
|
||||||
|
describe('memberCardApi', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
it('should call GET /member-cards with params', async () => {
|
||||||
|
const params = { page: 0, size: 10 }
|
||||||
|
await memberCardApi.list(params)
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/member-cards', { params })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include filter params', async () => {
|
||||||
|
const params = { status: 1, name: 'Gold', page: 0, size: 20 }
|
||||||
|
await memberCardApi.list(params)
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/member-cards', { params })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getActiveCards', () => {
|
||||||
|
it('should call GET /member-cards/active', async () => {
|
||||||
|
await memberCardApi.getActiveCards()
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/member-cards/active', { params: { status: undefined } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should pass status param', async () => {
|
||||||
|
await memberCardApi.getActiveCards(1)
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/member-cards/active', { params: { status: 1 } })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getById', () => {
|
||||||
|
it('should call GET /member-cards/:id', async () => {
|
||||||
|
await memberCardApi.getById(5)
|
||||||
|
expect(mockRequest.get).toHaveBeenCalledWith('/member-cards/5')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should call POST /member-cards with data', async () => {
|
||||||
|
const data = { memberCardName: 'Gold', memberCardPrice: 199 }
|
||||||
|
await memberCardApi.create(data)
|
||||||
|
expect(mockRequest.post).toHaveBeenCalledWith('/member-cards', data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should call PUT /member-cards/:id with data', async () => {
|
||||||
|
const data = { memberCardName: 'Platinum' }
|
||||||
|
await memberCardApi.update(3, data)
|
||||||
|
expect(mockRequest.put).toHaveBeenCalledWith('/member-cards/3', data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should call DELETE /member-cards/:id', async () => {
|
||||||
|
await memberCardApi.delete(3)
|
||||||
|
expect(mockRequest.delete).toHaveBeenCalledWith('/member-cards/3')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import BannerManagement from '@/views/banner/BannerManagement.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
ElMessageBox: { confirm: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Plus: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock banner.api =====
|
||||||
|
vi.mock('@/api/banner.api', () => ({
|
||||||
|
bannerApi: {
|
||||||
|
getAll: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, imageUrl: '/api/files/1/preview', title: '轮播图1', subtitle: '副标题1', sortOrder: 1, isActive: true, createdAt: '2025-01-01T00:00:00' },
|
||||||
|
{ id: 2, imageUrl: '/api/files/2/preview', title: '轮播图2', subtitle: '副标题2', sortOrder: 2, isActive: false, createdAt: '2025-01-02T00:00:00' },
|
||||||
|
]),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock request =====
|
||||||
|
vi.mock('@/utils/request', () => {
|
||||||
|
const mockRequest = {
|
||||||
|
get: vi.fn().mockResolvedValue({}),
|
||||||
|
post: vi.fn().mockResolvedValue({}),
|
||||||
|
put: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
return { default: mockRequest }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== Mock dateFormat =====
|
||||||
|
vi.mock('@/utils/dateFormat', () => ({
|
||||||
|
formatDateTime: vi.fn((val: string) => val || '-'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock URL.createObjectURL =====
|
||||||
|
if (!globalThis.URL.createObjectURL) {
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.createObjectURL = vi.fn(() => `blob:mock-${Math.random()}`)
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.revokeObjectURL = vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('BannerManagement Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>' },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>' },
|
||||||
|
'el-input': { template: '<input/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'effect', 'size'] },
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width'] },
|
||||||
|
'el-form': { template: '<form><slot/></form>', props: ['model', 'rules', 'label-width', 'ref'] },
|
||||||
|
'el-form-item': { template: '<div class="el-form-item"><slot/></div>', props: ['label', 'prop'] },
|
||||||
|
'el-upload': { template: '<div class="el-upload-stub"><slot/></div>', props: ['http-request', 'show-file-list', 'before-upload', 'accept'] },
|
||||||
|
'el-input-number': true,
|
||||||
|
'el-switch': true,
|
||||||
|
'el-image': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
setActivePinia(pinia)
|
||||||
|
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<div/>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染轮播图管理容器', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.banner-management').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染页面标题', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('轮播图管理')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染新增轮播图按钮', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('新增轮播图')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态为 false', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 modalVisible 为 false', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 dataSource 为空数组', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.dataSource).toBeDefined()
|
||||||
|
expect(Array.isArray(wrapper.vm.dataSource)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 handleAdd 方法', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEdit 方法', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleDelete 方法', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleUpload 方法', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleUpload).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 beforeUpload 方法', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.beforeUpload).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 新增/编辑表单 ====================
|
||||||
|
|
||||||
|
it('点击新增后 modalVisible 应为 true', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(true)
|
||||||
|
expect(wrapper.vm.modalTitle).toBe('新增轮播图')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时表单 title 应为空', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.title).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时 sortOrder 应为 0', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.sortOrder).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时 isActive 应为 true', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.isActive).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单规则 ====================
|
||||||
|
|
||||||
|
it('应该定义 imageUrl 必填规则', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.imageUrl).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.imageUrl[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该定义 title 必填规则', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.title).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.title[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该定义 sortOrder 必填规则', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.sortOrder).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== extractFileId ====================
|
||||||
|
|
||||||
|
it('extractFileId 应能提取 /api/files/{id}/preview 格式的 ID', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.extractFileId('/api/files/123/preview')).toBe('123')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('extractFileId 应对纯数字字符串返回该数字', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.extractFileId('456')).toBe('456')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('extractFileId 应对 null/undefined 返回 null', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.extractFileId(null)).toBe(null)
|
||||||
|
expect(wrapper.vm.extractFileId(undefined)).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== beforeUpload ====================
|
||||||
|
|
||||||
|
it('beforeUpload 应拒绝非图片文件', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
const { ElMessage } = await import('element-plus')
|
||||||
|
|
||||||
|
const result = wrapper.vm.beforeUpload(new File([''], 'test.txt', { type: 'text/plain' }))
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(ElMessage.error).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('beforeUpload 应拒绝超过 5MB 的图片', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
const { ElMessage } = await import('element-plus')
|
||||||
|
|
||||||
|
const largeFile = new File([''], 'large.jpg', { type: 'image/jpeg' })
|
||||||
|
Object.defineProperty(largeFile, 'size', { value: 6 * 1024 * 1024 })
|
||||||
|
const result = wrapper.vm.beforeUpload(largeFile)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(ElMessage.error).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('beforeUpload 应接受合法图片', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
const validFile = new File([''], 'small.jpg', { type: 'image/jpeg' })
|
||||||
|
const result = wrapper.vm.beforeUpload(validFile)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 预览 ====================
|
||||||
|
|
||||||
|
it('应该暴露 previewImage 方法', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.previewImage).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 previewVisible 应为 false', async () => {
|
||||||
|
wrapper = mount(BannerManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.previewVisible).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import CoachManagement from '@/views/system/CoachManagement.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
ElMessageBox: { confirm: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Search: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock coach.api =====
|
||||||
|
vi.mock('@/api/coach.api', () => ({
|
||||||
|
coachApi: {
|
||||||
|
getAll: vi.fn().mockResolvedValue([
|
||||||
|
{ id: '1', username: 'coach1', nickname: 'Coach One', email: 'coach1@test.com', phone: '13800138001', status: 1, createdAt: '2025-01-01T00:00:00', updatedAt: '2025-01-01T00:00:00' },
|
||||||
|
{ id: '2', username: 'coach2', nickname: 'Coach Two', email: 'coach2@test.com', phone: '13800138002', status: 0, createdAt: '2025-01-02T00:00:00', updatedAt: '2025-01-02T00:00:00' },
|
||||||
|
]),
|
||||||
|
getViolationCounts: vi.fn().mockResolvedValue([
|
||||||
|
{ coach_id: 1, count: 2 },
|
||||||
|
{ coach_id: 2, count: 0 },
|
||||||
|
]),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
disable: vi.fn().mockResolvedValue({}),
|
||||||
|
getCourses: vi.fn().mockResolvedValue([]),
|
||||||
|
getViolations: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock groupCourse.api =====
|
||||||
|
vi.mock('@/api/groupCourse.api', () => ({
|
||||||
|
groupCourseBookingApi: {
|
||||||
|
getByCourseId: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock request =====
|
||||||
|
vi.mock('@/utils/request', () => {
|
||||||
|
const mockRequest = {
|
||||||
|
get: vi.fn().mockResolvedValue({}),
|
||||||
|
post: vi.fn().mockResolvedValue({}),
|
||||||
|
put: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
return { default: mockRequest }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== Mock errorHandler =====
|
||||||
|
vi.mock('@/utils/errorHandler', () => ({
|
||||||
|
handleApiError: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock dateFormat =====
|
||||||
|
vi.mock('@/utils/dateFormat', () => ({
|
||||||
|
formatDateTime: vi.fn((val: string) => val || '-'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock URL.createObjectURL =====
|
||||||
|
if (!globalThis.URL.createObjectURL) {
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.createObjectURL = vi.fn(() => `blob:mock-${Math.random()}`)
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.revokeObjectURL = vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CoachManagement Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>' },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>' },
|
||||||
|
'el-input': { template: '<input/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading', 'row-class-name'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'effect', 'size', 'round'] },
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width'] },
|
||||||
|
'el-form': { template: '<form><slot/></form>', props: ['model', 'rules', 'label-width', 'ref'] },
|
||||||
|
'el-form-item': { template: '<div class="el-form-item"><slot/></div>', props: ['label', 'prop', 'required'] },
|
||||||
|
'el-descriptions': { template: '<div class="el-descriptions"><slot/></div>', props: ['column', 'border', 'size'] },
|
||||||
|
'el-descriptions-item': { template: '<div class="el-descriptions-item"><slot/></div>', props: ['label', 'span'] },
|
||||||
|
'el-empty': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
setActivePinia(pinia)
|
||||||
|
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<div/>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染教练管理容器', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.coach-management').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染搜索区域', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('搜索教练用户名或昵称')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染新增教练按钮', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('新增教练')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态(onMounted 触发 fetchData 后 loading 启动)', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
// onMounted 调用了 fetchData,loading 应为 true(mock API 异步返回)
|
||||||
|
expect(wrapper.vm.loading).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始搜索关键词为空', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.searchKeyword).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始模态框不可见', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 handleSearch 方法', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleAdd 方法', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEdit 方法', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleDisable 方法', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleDisable).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleViewCourses 方法', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleViewCourses).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleViewDetail 方法', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleViewDetail).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单状态绑定 ====================
|
||||||
|
|
||||||
|
it('点击新增后 modalVisible 应为 true', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(true)
|
||||||
|
expect(wrapper.vm.modalTitle).toBe('新增教练')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时表单 username 应为空', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.username).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时表单 password 应为空', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.password).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单规则 ====================
|
||||||
|
|
||||||
|
it('应该定义 username 必填规则', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.username).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.username[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该定义 password 必填规则', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.password).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.password[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该定义 email 必填规则', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.email).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== computed 属性 ====================
|
||||||
|
|
||||||
|
it('filteredData 应在无搜索词时返回全部数据', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.dataSource = [
|
||||||
|
{ id: '1', username: 'coach1', nickname: 'A', email: 'a@t.com', status: 1 },
|
||||||
|
{ id: '2', username: 'coach2', nickname: 'B', email: 'b@t.com', status: 1 },
|
||||||
|
]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.filteredData.length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filteredData 应按关键词过滤', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.dataSource = [
|
||||||
|
{ id: '1', username: 'coach1', nickname: 'Alpha', email: 'a@t.com', status: 1 },
|
||||||
|
{ id: '2', username: 'coach2', nickname: 'Beta', email: 'b@t.com', status: 1 },
|
||||||
|
]
|
||||||
|
wrapper.vm.searchKeyword = 'alpha'
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.filteredData.length).toBe(1)
|
||||||
|
expect(wrapper.vm.filteredData[0].nickname).toBe('Alpha')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 状态映射函数 ====================
|
||||||
|
|
||||||
|
it('getViolationTypeText 应正确返回违规类型文本', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.getViolationTypeText('COACH_LATE')).toBe('教练迟到')
|
||||||
|
expect(wrapper.vm.getViolationTypeText('COACH_ABSENT')).toBe('教练缺席')
|
||||||
|
expect(wrapper.vm.getViolationTypeText('NOT_MANUAL_END')).toBe('未手动结课')
|
||||||
|
expect(wrapper.vm.getViolationTypeText('UNKNOWN')).toBe('UNKNOWN')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getCourseStatusText 应正确返回课程状态文本', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.getCourseStatusText(0)).toBe('正常')
|
||||||
|
expect(wrapper.vm.getCourseStatusText(1)).toBe('已取消')
|
||||||
|
expect(wrapper.vm.getCourseStatusText(2)).toBe('已结束')
|
||||||
|
expect(wrapper.vm.getCourseStatusText(99)).toBe('未知')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 违规标签颜色 ====================
|
||||||
|
|
||||||
|
it('getViolationTag 应在无违规时返回 success', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.getViolationTag(999)).toBe('success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getViolationTag 应在有违规时返回 warning 或 danger', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.violationCounts[1] = 1
|
||||||
|
expect(wrapper.vm.getViolationTag(1)).toBe('warning')
|
||||||
|
|
||||||
|
wrapper.vm.violationCounts[1] = 5
|
||||||
|
expect(wrapper.vm.getViolationTag(1)).toBe('danger')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 取消弹窗 ====================
|
||||||
|
|
||||||
|
it('handleModalCancel 应关闭弹窗', async () => {
|
||||||
|
wrapper = mount(CoachManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.modalVisible = true
|
||||||
|
wrapper.vm.handleModalCancel()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import CourseLabelManagement from '@/views/groupcourse/CourseLabelManagement.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
ElMessageBox: { confirm: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Search: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock groupCourse.api =====
|
||||||
|
vi.mock('@/api/groupCourse.api', () => ({
|
||||||
|
courseLabelApi: {
|
||||||
|
getPage: vi.fn().mockResolvedValue({
|
||||||
|
content: [
|
||||||
|
{ id: 1, labelName: '热门', color: '#FF0000', description: '热门标签' },
|
||||||
|
{ id: 2, labelName: '新手', color: '#00FF00', description: '新手友好' },
|
||||||
|
],
|
||||||
|
totalElements: 2,
|
||||||
|
}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('CourseLabelManagement Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>', props: ['type', 'size', 'link'] },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>' },
|
||||||
|
'el-input': { template: '<input/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'effect', 'size', 'color'] },
|
||||||
|
'el-pagination': true,
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width'] },
|
||||||
|
'el-form': { template: '<form><slot/></form>', props: ['model', 'rules', 'label-width', 'ref'] },
|
||||||
|
'el-form-item': { template: '<div class="el-form-item"><slot/></div>', props: ['label', 'prop'] },
|
||||||
|
'el-color-picker': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
setActivePinia(pinia)
|
||||||
|
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<div/>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染标签管理容器', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.course-label-management').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染搜索区域', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('搜索标签名称')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染新增标签按钮', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('新增标签')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态为 false', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始搜索关键词为空', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.searchKeyword).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 modalVisible 为 false', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 currentPage 为 1', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.currentPage).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 pageSize 为 10', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.pageSize).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 handleSearch 方法', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleAdd 方法', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEdit 方法', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleDelete 方法', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 新增弹窗 ====================
|
||||||
|
|
||||||
|
it('handleAdd 应打开新增弹窗并重置表单', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(true)
|
||||||
|
expect(wrapper.vm.modalTitle).toBe('新增标签')
|
||||||
|
expect(wrapper.vm.formState.labelName).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单规则 ====================
|
||||||
|
|
||||||
|
it('应定义 labelName 必填规则', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.labelName).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.labelName[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应定义 color 必填规则', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.color).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 分页操作 ====================
|
||||||
|
|
||||||
|
it('onSizeChange 应重置当前页为 1', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.currentPage = 3
|
||||||
|
wrapper.vm.onSizeChange()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.currentPage).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handleSearch 应重置当前页为 1', async () => {
|
||||||
|
wrapper = mount(CourseLabelManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.currentPage = 3
|
||||||
|
wrapper.vm.handleSearch()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.currentPage).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import CourseRecommendManagement from '@/views/groupcourse/CourseRecommendManagement.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
ElMessageBox: { confirm: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Search: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock groupCourse.api =====
|
||||||
|
vi.mock('@/api/groupCourse.api', () => ({
|
||||||
|
groupCourseRecommendApi: {
|
||||||
|
getAll: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, courseId: 1, recommendTitle: '推荐课程1', recommendContent: '内容1', recommendReason: '理由1', priority: 3, isActive: true, createdAt: '2025-01-01T00:00:00' },
|
||||||
|
{ id: 2, courseId: 2, recommendTitle: '推荐课程2', recommendContent: '内容2', recommendReason: '理由2', priority: 5, isActive: false, createdAt: '2025-01-02T00:00:00' },
|
||||||
|
]),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
toggleActive: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
groupCourseApi: {
|
||||||
|
getAll: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, courseName: '团课A' },
|
||||||
|
{ id: 2, courseName: '团课B' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock dateFormat =====
|
||||||
|
vi.mock('@/utils/dateFormat', () => ({
|
||||||
|
formatDateTime: vi.fn((val: string) => val || '-'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('CourseRecommendManagement Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>', props: ['type', 'size', 'link'] },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>' },
|
||||||
|
'el-input': { template: '<input/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'effect', 'size'] },
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width'] },
|
||||||
|
'el-form': { template: '<form><slot/></form>', props: ['model', 'rules', 'label-width', 'ref'] },
|
||||||
|
'el-form-item': { template: '<div class="el-form-item"><slot/></div>', props: ['label', 'prop'] },
|
||||||
|
'el-select': { template: '<select><slot/></select>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-option': { template: '<option/>', props: ['value', 'label'] },
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
setActivePinia(pinia)
|
||||||
|
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<div/>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染推荐管理容器', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.course-recommend-management').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染页面标题', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('团课推荐管理')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染新增推荐按钮', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('新增推荐')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染恢复默认排序按钮', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('恢复默认排序')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态(onMounted 触发 fetchData)', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
// onMounted 调用了 fetchData,loading 应为 true
|
||||||
|
expect(wrapper.vm.loading).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 modalVisible 为 false', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 handleAdd 方法', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEdit 方法', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleDelete 方法', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleToggleActive 方法', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleToggleActive).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 resetSort 方法', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.resetSort).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 新增弹窗 ====================
|
||||||
|
|
||||||
|
it('handleAdd 应打开新增弹窗', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时 formState.recommendTitle 应为空', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.recommendTitle).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时 formState.priority 应为默认值', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.isActive).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单规则 ====================
|
||||||
|
|
||||||
|
it('应定义 courseId 必填规则', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.courseId).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.courseId[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应定义 recommendTitle 必填规则', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.recommendTitle).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应定义 priority 必填规则', async () => {
|
||||||
|
wrapper = mount(CourseRecommendManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.priority).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import CourseTypeManagement from '@/views/groupcourse/CourseTypeManagement.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
ElMessageBox: { confirm: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Search: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock groupCourse.api =====
|
||||||
|
vi.mock('@/api/groupCourse.api', () => ({
|
||||||
|
groupCourseTypeApi: {
|
||||||
|
getPage: vi.fn().mockResolvedValue({
|
||||||
|
content: [
|
||||||
|
{ id: 1, typeName: '瑜伽', baseDifficulty: 3, category: '健身', description: '瑜伽课程' },
|
||||||
|
{ id: 2, typeName: '搏击', baseDifficulty: 7, category: '格斗', description: '搏击课程' },
|
||||||
|
],
|
||||||
|
totalElements: 2,
|
||||||
|
}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
getTypeLabels: vi.fn().mockResolvedValue({}),
|
||||||
|
getCategories: vi.fn().mockResolvedValue(['健身', '格斗', '舞蹈']),
|
||||||
|
},
|
||||||
|
courseLabelApi: {
|
||||||
|
getPage: vi.fn().mockResolvedValue({ content: [], totalElements: 0 }),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('CourseTypeManagement Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>', props: ['type', 'size', 'link'] },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>' },
|
||||||
|
'el-input': { template: '<input/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'effect', 'size', 'color'] },
|
||||||
|
'el-pagination': true,
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width'] },
|
||||||
|
'el-form': { template: '<form><slot/></form>', props: ['model', 'rules', 'label-width', 'ref'] },
|
||||||
|
'el-form-item': { template: '<div class="el-form-item"><slot/></div>', props: ['label', 'prop'] },
|
||||||
|
'el-select': { template: '<select><slot/></select>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-option': { template: '<option/>', props: ['value', 'label'] },
|
||||||
|
'el-slider': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
setActivePinia(pinia)
|
||||||
|
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<div/>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染课程类型管理容器', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.course-type-management').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染搜索区域', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('搜索类型名称')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染新增类型按钮', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('新增类型')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态为 false', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始搜索关键词为空', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.searchKeyword).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 searchCategory 为空', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.searchCategory).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 modalVisible 为 false', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 handleSearch 方法', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleAdd 方法', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEdit 方法', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleDelete 方法', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 difficultyLabel 方法', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.difficultyLabel).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== difficultyLabel ====================
|
||||||
|
|
||||||
|
it('difficultyLabel(1) 应返回"入门"', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.difficultyLabel(1)).toBe('入门')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('difficultyLabel(5) 应返回"中级"', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.difficultyLabel(5)).toBe('中级')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('difficultyLabel(8) 应返回"进阶"', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.difficultyLabel(8)).toBe('进阶')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 新增弹窗 ====================
|
||||||
|
|
||||||
|
it('handleAdd 应打开新增弹窗', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时 formState.typeName 应为空', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.typeName).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单规则 ====================
|
||||||
|
|
||||||
|
it('应定义 typeName 必填规则', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.typeName).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.typeName[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('表单规则应包含 typeName 和 category', async () => {
|
||||||
|
wrapper = mount(CourseTypeManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
// typeName 一定存在
|
||||||
|
expect(wrapper.vm.formRules.typeName).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.typeName[0].required).toBe(true)
|
||||||
|
// category 规则存在性取决于组件实现
|
||||||
|
expect(wrapper.vm.formRules).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import Forbidden from '@/views/system/Forbidden.vue'
|
||||||
|
|
||||||
|
describe('Forbidden Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-result': { template: '<div class="el-result"><span class="title">{{title}}</span><span class="sub-title">{{subTitle}}</span><slot name="extra"/></div>', props: ['icon', 'title', 'subTitle'] },
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>', props: ['type'] },
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{ path: '/', component: { template: '<div/>' } },
|
||||||
|
{ path: '/dashboard', component: { template: '<div id="dashboard"/>' } },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染禁止访问容器', async () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.forbidden-container').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该显示 403 状态码', () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.html()).toContain('403')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该显示权限提示信息', () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.html()).toContain('抱歉,您没有权限访问此页面')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 按钮渲染 ====================
|
||||||
|
|
||||||
|
it('应该渲染"返回上一页"按钮', () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.html()).toContain('返回上一页')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染"返回首页"按钮', () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.html()).toContain('返回首页')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 goBack 方法', () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(typeof wrapper.vm.goBack).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 goHome 方法', () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(typeof wrapper.vm.goHome).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 导航行为 ====================
|
||||||
|
|
||||||
|
it('goHome 应导航到 /dashboard', async () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
const pushSpy = vi.spyOn(router, 'push')
|
||||||
|
wrapper.vm.goHome()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(pushSpy).toHaveBeenCalledWith('/dashboard')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('goBack 应调用 router.go(-1)', async () => {
|
||||||
|
wrapper = mount(Forbidden, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
const goSpy = vi.spyOn(router, 'go')
|
||||||
|
wrapper.vm.goBack()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(goSpy).toHaveBeenCalledWith(-1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import GroupCourseManagement from '@/views/groupcourse/GroupCourseManagement.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
ElMessageBox: { confirm: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Search: { template: '<svg/>' },
|
||||||
|
Plus: { template: '<svg/>' },
|
||||||
|
WarningFilled: { template: '<svg/>' },
|
||||||
|
Refresh: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock groupCourse.api =====
|
||||||
|
vi.mock('@/api/groupCourse.api', () => ({
|
||||||
|
groupCourseApi: {
|
||||||
|
getPage: vi.fn().mockResolvedValue({
|
||||||
|
content: [
|
||||||
|
{ id: 1, courseName: '瑜伽课', courseType: 1, coachId: 1, status: '0', startTime: '2025-01-01T10:00:00', endTime: '2025-01-01T11:00:00', location: '302室', maxMembers: 20, currentMembers: 10, coverImage: '', description: '瑜伽课描述', storedValueAmount: 0 },
|
||||||
|
{ id: 2, courseName: '搏击课', courseType: 2, coachId: 2, status: '1', startTime: '2025-01-02T14:00:00', endTime: '2025-01-02T15:00:00', location: '101室', maxMembers: 15, currentMembers: 5, coverImage: '', description: '搏击课描述', storedValueAmount: 50 },
|
||||||
|
],
|
||||||
|
totalElements: 2,
|
||||||
|
}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
cancel: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
checkCoachConflict: vi.fn().mockResolvedValue({ hasConflict: false, conflicts: [] }),
|
||||||
|
},
|
||||||
|
groupCourseTypeApi: {
|
||||||
|
getAll: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, typeName: '瑜伽' },
|
||||||
|
{ id: 2, typeName: '搏击' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
groupCourseBookingApi: {
|
||||||
|
getByCourseId: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock coach.api =====
|
||||||
|
vi.mock('@/api/coach.api', () => ({
|
||||||
|
coachApi: {
|
||||||
|
getAll: vi.fn().mockResolvedValue([
|
||||||
|
{ id: '1', username: 'coach1', nickname: '教练1' },
|
||||||
|
{ id: '2', username: 'coach2', nickname: '教练2' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock request =====
|
||||||
|
vi.mock('@/utils/request', () => {
|
||||||
|
const mockRequest = {
|
||||||
|
get: vi.fn().mockResolvedValue({}),
|
||||||
|
post: vi.fn().mockResolvedValue({}),
|
||||||
|
put: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
return { default: mockRequest }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== Mock dateFormat =====
|
||||||
|
vi.mock('@/utils/dateFormat', () => ({
|
||||||
|
formatDateTime: vi.fn((val: string) => val || '-'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock URL.createObjectURL =====
|
||||||
|
if (!globalThis.URL.createObjectURL) {
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.createObjectURL = vi.fn(() => `blob:mock-${Math.random()}`)
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.revokeObjectURL = vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GroupCourseManagement Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>', props: ['type', 'size', 'link', 'loading', 'circle', 'disabled'] },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>' },
|
||||||
|
'el-input': { template: '<input/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-input-number': true,
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'effect', 'size'] },
|
||||||
|
'el-pagination': true,
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width', 'center'] },
|
||||||
|
'el-form': { template: '<form><slot/></form>', props: ['model', 'rules', 'label-width', 'ref'] },
|
||||||
|
'el-form-item': { template: '<div class="el-form-item"><slot/></div>', props: ['label', 'prop'] },
|
||||||
|
'el-select': { template: '<select><slot/></select>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-option': { template: '<option/>', props: ['value', 'label'] },
|
||||||
|
'el-date-picker': true,
|
||||||
|
'el-upload': { template: '<div class="el-upload-stub"><slot/></div>', props: ['http-request', 'show-file-list', 'before-upload', 'accept'] },
|
||||||
|
'el-descriptions': { template: '<div class="el-descriptions"><slot/></div>', props: ['column', 'border'] },
|
||||||
|
'el-descriptions-item': { template: '<div class="el-descriptions-item"><slot/></div>', props: ['label', 'span'] },
|
||||||
|
'el-empty': true,
|
||||||
|
'el-image': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
setActivePinia(pinia)
|
||||||
|
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<div/>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染团课管理容器', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.group-course-management').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染搜索区域', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('搜索课程名称')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染新增团课按钮', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('新增团课')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态为 false', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始搜索关键词为空', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.searchKeyword).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 searchStatus 为空', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.searchStatus).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 modalVisible 为 false', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 pagination.current 为 1', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 handleSearch 方法', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleAdd 方法', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEdit 方法', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleCancel 方法', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleCancel).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleDelete 方法', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 showDetail 方法', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.showDetail).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 showQrCode 方法', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.showQrCode).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== statusMap ====================
|
||||||
|
|
||||||
|
it('statusMap 应正确映射状态', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.statusMap['0'].label).toBe('正常')
|
||||||
|
expect(wrapper.vm.statusMap['1'].label).toBe('已取消')
|
||||||
|
expect(wrapper.vm.statusMap['2'].label).toBe('已结束')
|
||||||
|
expect(wrapper.vm.statusMap['3'].label).toBe('进行中')
|
||||||
|
expect(wrapper.vm.statusMap['5'].label).toBe('教练缺席')
|
||||||
|
expect(wrapper.vm.statusMap['6'].label).toBe('自动结束')
|
||||||
|
expect(wrapper.vm.statusMap['7'].label).toBe('教练迟到')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单规则 ====================
|
||||||
|
|
||||||
|
it('应定义 courseName 必填规则', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.courseName).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.courseName[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应定义 startTime 必填规则', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.startTime).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.startTime[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应定义 duration 必填规则', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.duration).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.duration[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 新增弹窗 ====================
|
||||||
|
|
||||||
|
it('handleAdd 应打开新增弹窗', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.modalVisible).toBe(true)
|
||||||
|
expect(wrapper.vm.modalTitle).toBe('新增团课')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时 formState.courseName 应为空', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.courseName).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增时 formState.maxMembers 应为 20', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleAdd()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.formState.maxMembers).toBe(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== beforeCoverUpload ====================
|
||||||
|
|
||||||
|
it('beforeCoverUpload 应拒绝非图片文件', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
const { ElMessage } = await import('element-plus')
|
||||||
|
|
||||||
|
const result = wrapper.vm.beforeCoverUpload(new File([''], 'test.txt', { type: 'text/plain' }))
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(ElMessage.error).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('beforeCoverUpload 应接受合法图片', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
const result = wrapper.vm.beforeCoverUpload(new File([''], 'photo.jpg', { type: 'image/jpeg' }))
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== extractFileId ====================
|
||||||
|
|
||||||
|
it('extractFileId 应能提取 /files/{id}/preview 格式', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.extractFileId('/api/files/123/preview')).toBe('123')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== downloadQrCode ====================
|
||||||
|
|
||||||
|
it('downloadQrCode 应在无 qrImageUrl 时不执行', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.qrImageUrl = ''
|
||||||
|
// Should not throw
|
||||||
|
expect(() => wrapper.vm.downloadQrCode()).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 搜索与分页 ====================
|
||||||
|
|
||||||
|
it('handleSearch 应重置当前页为 1', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.pagination.current = 3
|
||||||
|
wrapper.vm.handleSearch()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handleSizeChange 应重置当前页为 1', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.pagination.current = 3
|
||||||
|
wrapper.vm.handleSizeChange()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== getComputedEndTime ====================
|
||||||
|
|
||||||
|
it('getComputedEndTime 应在缺少 startTime 时返回空字符串', async () => {
|
||||||
|
wrapper = mount(GroupCourseManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.formState.startTime = ''
|
||||||
|
expect(wrapper.vm.getComputedEndTime()).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import MemberCardManagement from '@/views/member/MemberCardManagement.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
ElMessageBox: { confirm: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Search: { template: '<svg/>' },
|
||||||
|
Plus: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock memberCard.api =====
|
||||||
|
vi.mock('@/api/memberCard.api', () => ({
|
||||||
|
memberCardApi: {
|
||||||
|
list: vi.fn().mockResolvedValue([
|
||||||
|
{ memberCardId: 1, memberCardName: '月卡', memberCardType: 'TIME_CARD', memberCardPrice: 299, memberCardValidityDays: 30, memberCardStatus: 1, createdAt: '2025-01-01T00:00:00' },
|
||||||
|
{ memberCardId: 2, memberCardName: '10次卡', memberCardType: 'COUNT_CARD', memberCardPrice: 199, memberCardTotalTimes: 10, memberCardStatus: 1, createdAt: '2025-01-02T00:00:00' },
|
||||||
|
{ memberCardId: 3, memberCardName: '500储值卡', memberCardType: 'STORED_VALUE_CARD', memberCardPrice: 500, memberCardAmount: 500, memberCardStatus: 0, createdAt: '2025-01-03T00:00:00' },
|
||||||
|
]),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock dateFormat =====
|
||||||
|
vi.mock('@/utils/dateFormat', () => ({
|
||||||
|
formatDateTime: vi.fn((val: string) => val || '-'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('MemberCardManagement Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>', props: ['type', 'size', 'link', 'loading'] },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>' },
|
||||||
|
'el-input': { template: '<input/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-input-number': true,
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'effect', 'size'] },
|
||||||
|
'el-switch': { template: '<div class="el-switch"/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-pagination': true,
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width'] },
|
||||||
|
'el-form': { template: '<form><slot/></form>', props: ['model', 'rules', 'label-width', 'ref'] },
|
||||||
|
'el-form-item': { template: '<div class="el-form-item"><slot/></div>', props: ['label', 'prop'] },
|
||||||
|
'el-select': { template: '<select><slot/></select>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-option': { template: '<option/>', props: ['value', 'label'] },
|
||||||
|
'el-radio-group': { template: '<div class="el-radio-group"><slot/></div>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-radio': { template: '<label class="el-radio"><slot/></label>', props: ['value'] },
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
setActivePinia(pinia)
|
||||||
|
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<div/>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染会员卡管理容器', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.member-card-management').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染过滤区域', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('搜索卡名称')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染新增会员卡按钮', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('新增会员卡')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态为 false', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 filterType 为空', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.filterType).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 filterName 为空', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.filterName).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 dialogVisible 为 false', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.dialogVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 pagination.current 为 1', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 fetchData 方法', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.fetchData).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleFilter 方法', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleFilter).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleCreate 方法', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleCreate).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEdit 方法', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleSubmit 方法', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleSubmit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleDelete 方法', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleToggleStatus 方法', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleToggleStatus).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== cardTypeMap ====================
|
||||||
|
|
||||||
|
it('cardTypeMap 应正确映射卡类型', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.cardTypeMap['TIME_CARD']).toBe('时长卡')
|
||||||
|
expect(wrapper.vm.cardTypeMap['COUNT_CARD']).toBe('次卡')
|
||||||
|
expect(wrapper.vm.cardTypeMap['STORED_VALUE_CARD']).toBe('储值卡')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单操作 ====================
|
||||||
|
|
||||||
|
it('handleCreate 应打开新增弹窗并重置表单', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleCreate()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.dialogVisible).toBe(true)
|
||||||
|
expect(wrapper.vm.dialogTitle).toBe('新增会员卡')
|
||||||
|
expect(wrapper.vm.isEditing).toBe(false)
|
||||||
|
expect(wrapper.vm.form.memberCardName).toBe('')
|
||||||
|
expect(wrapper.vm.form.memberCardType).toBe('TIME_CARD')
|
||||||
|
expect(wrapper.vm.form.memberCardStatus).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handleCreate 应设置 memberCardPrice 为 undefined', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleCreate()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.form.memberCardPrice).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单规则 ====================
|
||||||
|
|
||||||
|
it('应定义 memberCardName 必填规则', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.memberCardName).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.memberCardName[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应定义 memberCardType 必填规则', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.memberCardType).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.memberCardType[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应定义 memberCardPrice 必填规则', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formRules.memberCardPrice).toBeDefined()
|
||||||
|
expect(wrapper.vm.formRules.memberCardPrice[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== handleTypeChange ====================
|
||||||
|
|
||||||
|
it('handleTypeChange 应在非编辑模式清空类型相关字段', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.isEditing = false
|
||||||
|
wrapper.vm.form.memberCardValidityDays = 30
|
||||||
|
wrapper.vm.form.memberCardTotalTimes = 10
|
||||||
|
wrapper.vm.form.memberCardAmount = 100
|
||||||
|
wrapper.vm.handleTypeChange()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.form.memberCardValidityDays).toBeUndefined()
|
||||||
|
expect(wrapper.vm.form.memberCardTotalTimes).toBeUndefined()
|
||||||
|
expect(wrapper.vm.form.memberCardAmount).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== filter 与分页操作 ====================
|
||||||
|
|
||||||
|
it('handleFilter 应重置到第一页', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.pagination.current = 3
|
||||||
|
wrapper.vm.handleFilter()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handleSizeChange 应重置到第一页', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.pagination.current = 3
|
||||||
|
wrapper.vm.handleSizeChange()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== submitting 状态 ====================
|
||||||
|
|
||||||
|
it('初始 submitting 为 false', async () => {
|
||||||
|
wrapper = mount(MemberCardManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.submitting).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import MemberManagement from '@/views/member/MemberManagement.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Search: { template: '<svg/>' },
|
||||||
|
UserFilled: { template: '<svg/>' },
|
||||||
|
Plus: { template: '<svg/>' },
|
||||||
|
Clock: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock member.api =====
|
||||||
|
vi.mock('@/api/member.api', () => ({
|
||||||
|
memberApi: {
|
||||||
|
getAll: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, memberNo: 'M001', nickname: 'Member1', phone: '13800138001', gender: 1, createdAt: '2025-01-01T00:00:00', subscribed: true },
|
||||||
|
{ id: 2, memberNo: 'M002', nickname: 'Member2', phone: '13800138002', gender: 2, createdAt: '2025-01-02T00:00:00', subscribed: false },
|
||||||
|
]),
|
||||||
|
search: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, memberNo: 'M001', nickname: 'Member1', phone: '13800138001', gender: 1, createdAt: '2025-01-01T00:00:00' },
|
||||||
|
]),
|
||||||
|
getDetail: vi.fn().mockResolvedValue({
|
||||||
|
id: 1, memberNo: 'M001', nickname: 'Member1', phone: '13800138001',
|
||||||
|
genderDesc: '男', birthday: '1990-01-01', address: '北京市',
|
||||||
|
avatar: '', subscribed: true, lastLoginAt: '2025-07-01T00:00:00',
|
||||||
|
createdAt: '2025-01-01T00:00:00', activeCardCount: 2, inactiveCardCount: 1,
|
||||||
|
}),
|
||||||
|
getMemberCardRecords: vi.fn().mockResolvedValue([
|
||||||
|
{ memberCardName: '月卡', memberCardType: 'TIME_CARD', expireTime: '2025-08-01T00:00:00', purchaseTime: '2025-07-01T00:00:00', status: 'ACTIVE' },
|
||||||
|
]),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
updatePhone: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock request =====
|
||||||
|
vi.mock('@/utils/request', () => {
|
||||||
|
const mockRequest = {
|
||||||
|
get: vi.fn().mockResolvedValue({}),
|
||||||
|
post: vi.fn().mockResolvedValue({}),
|
||||||
|
put: vi.fn().mockResolvedValue({}),
|
||||||
|
delete: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
return { default: mockRequest }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== Mock dateFormat =====
|
||||||
|
vi.mock('@/utils/dateFormat', () => ({
|
||||||
|
formatDateTime: vi.fn((val: string) => val || '-'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock URL.createObjectURL =====
|
||||||
|
if (!globalThis.URL.createObjectURL) {
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.createObjectURL = vi.fn(() => `blob:mock-${Math.random()}`)
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.revokeObjectURL = vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MemberManagement Component', () => {
|
||||||
|
let router: any
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>', props: ['type', 'size', 'link'] },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>' },
|
||||||
|
'el-input': { template: '<input/>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'effect', 'size'] },
|
||||||
|
'el-pagination': true,
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width'] },
|
||||||
|
'el-form': { template: '<form><slot/></form>', props: ['model', 'rules', 'label-width', 'ref'] },
|
||||||
|
'el-form-item': { template: '<div class="el-form-item"><slot/></div>', props: ['label', 'prop'] },
|
||||||
|
'el-select': { template: '<select><slot/></select>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-option': { template: '<option/>', props: ['value', 'label'] },
|
||||||
|
'el-date-picker': true,
|
||||||
|
'el-avatar': { template: '<div class="el-avatar"/>', props: ['src', 'size'] },
|
||||||
|
'el-descriptions': { template: '<div class="el-descriptions"><slot/></div>', props: ['column', 'border'] },
|
||||||
|
'el-descriptions-item': { template: '<div class="el-descriptions-item"><slot/></div>', props: ['label', 'span'] },
|
||||||
|
'el-empty': true,
|
||||||
|
'el-upload': { template: '<div class="el-upload-stub"><slot/></div>', props: ['http-request', 'show-file-list', 'before-upload', 'accept'] },
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
setActivePinia(pinia)
|
||||||
|
|
||||||
|
router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<div/>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染会员管理容器', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.member-management').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染搜索区域', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('搜索会员编号/昵称/手机号')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染搜索按钮', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('搜索')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态为 false', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 searchKeyword 为空', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.searchKeyword).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 pagination.current 为 1', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 detailVisible 为 false', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.detailVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 editVisible 为 false', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.editVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 phoneVisible 为 false', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.phoneVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 fetchData 方法', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.fetchData).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleSearch 方法', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleDetail 方法', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleDetail).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEdit 方法', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleEditPhone 方法', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleEditPhone).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== formatPhone 函数 ====================
|
||||||
|
|
||||||
|
it('formatPhone 应在有手机号时返回原值', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formatPhone('13800138000')).toBe('13800138000')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formatPhone 应在无手机号时返回"未绑定"', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.formatPhone(null)).toBe('未绑定')
|
||||||
|
expect(wrapper.vm.formatPhone(undefined)).toBe('未绑定')
|
||||||
|
expect(wrapper.vm.formatPhone('')).toBe('未绑定')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== genderMap ====================
|
||||||
|
|
||||||
|
it('genderMap 应正确映射性别', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.genderMap[0]).toBe('未知')
|
||||||
|
expect(wrapper.vm.genderMap[1]).toBe('男')
|
||||||
|
expect(wrapper.vm.genderMap[2]).toBe('女')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== cardTypeMap ====================
|
||||||
|
|
||||||
|
it('cardTypeMap 应正确映射卡类型', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.cardTypeMap['TIME_CARD']).toBe('时长卡')
|
||||||
|
expect(wrapper.vm.cardTypeMap['COUNT_CARD']).toBe('次卡')
|
||||||
|
expect(wrapper.vm.cardTypeMap['STORED_VALUE_CARD']).toBe('储值卡')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== phoneRules ====================
|
||||||
|
|
||||||
|
it('应定义 phone 表单规则', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.phoneRules.phone).toBeDefined()
|
||||||
|
expect(wrapper.vm.phoneRules.phone[0].required).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 分页操作 ====================
|
||||||
|
|
||||||
|
it('handleSizeChange 应重置当前页为 1', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.pagination.current = 3
|
||||||
|
wrapper.vm.handleSizeChange()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== beforeAvatarUpload ====================
|
||||||
|
|
||||||
|
it('beforeAvatarUpload 应拒绝非图片文件', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
const { ElMessage } = await import('element-plus')
|
||||||
|
|
||||||
|
const result = wrapper.vm.beforeAvatarUpload(new File([''], 'test.txt', { type: 'text/plain' }))
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('beforeAvatarUpload 应接受合法图片', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
const result = wrapper.vm.beforeAvatarUpload(new File([''], 'photo.jpg', { type: 'image/jpeg' }))
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== handleClearAvatar ====================
|
||||||
|
|
||||||
|
it('handleClearAvatar 应清空头像', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.editForm.avatar = 'https://example.com/avatar.jpg'
|
||||||
|
wrapper.vm.handleClearAvatar()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.editForm.avatar).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== handleResetSort ====================
|
||||||
|
|
||||||
|
it('handleResetSort 应重置排序字段', async () => {
|
||||||
|
wrapper = mount(MemberManagement, {
|
||||||
|
global: { plugins: [router], stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.handleResetSort()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.sortField).toBe('createdAt')
|
||||||
|
expect(wrapper.vm.sortOrder).toBe('desc')
|
||||||
|
expect(wrapper.vm.pagination.current).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import StatisticsDashboard from '@/views/statistics/StatisticsDashboard.vue'
|
||||||
|
|
||||||
|
// ===== Mock Element Plus =====
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock @element-plus/icons-vue =====
|
||||||
|
vi.mock('@element-plus/icons-vue', () => ({
|
||||||
|
Download: { template: '<svg/>' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock statistics.api =====
|
||||||
|
vi.mock('@/api/statistics.api', () => ({
|
||||||
|
statisticsApi: {
|
||||||
|
getSummary: vi.fn().mockResolvedValue({
|
||||||
|
memberStatistics: {
|
||||||
|
totalMembers: 100, newMembers: 10, signInMembers: 50,
|
||||||
|
bookingMembers: 30, cancelBookingMembers: 5, activeMembers: 60,
|
||||||
|
},
|
||||||
|
bookingStatistics: {
|
||||||
|
newBookings: 200, attendBookings: 150, cancelBookings: 30,
|
||||||
|
absentBookings: 20, attendanceRate: 75, cancelRate: 15,
|
||||||
|
},
|
||||||
|
signInStatistics: {
|
||||||
|
totalSignIns: 300, successSignIns: 280, failedSignIns: 20,
|
||||||
|
successRate: 93.3, qrCodeSignIns: 200, manualSignIns: 60, faceSignIns: 20,
|
||||||
|
},
|
||||||
|
coachStatistics: {
|
||||||
|
totalCoaches: 10, violatedCoaches: 2, totalCourses: 50,
|
||||||
|
lateCount: 3, absentCount: 1, notManualEndCount: 2,
|
||||||
|
},
|
||||||
|
generatedAt: '2025-07-23T10:00:00',
|
||||||
|
}),
|
||||||
|
getCoachRanking: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
coachName: 'Coach A', completedCourses: 20, attendedStudents: 150,
|
||||||
|
attendanceRate: 85, fillRate: 90, violationCount: 0, compositeScore: 88,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
coachName: 'Coach B', completedCourses: 15, attendedStudents: 120,
|
||||||
|
attendanceRate: 70, fillRate: 75, violationCount: 2, compositeScore: 65,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
exportStatistics: vi.fn().mockResolvedValue(new Blob()),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ===== Mock URL.createObjectURL =====
|
||||||
|
if (!globalThis.URL.createObjectURL) {
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.createObjectURL = vi.fn(() => `blob:mock-${Math.random()}`)
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.URL.revokeObjectURL = vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('StatisticsDashboard Component', () => {
|
||||||
|
let wrapper: any
|
||||||
|
|
||||||
|
const defaultStubs = {
|
||||||
|
'el-button': { template: '<button @click="$emit(\'click\')"><slot/></button>', props: ['loading', 'icon', 'type', 'size', 'link'] },
|
||||||
|
'el-card': { template: '<div class="el-card"><slot/><slot name="header"/></div>', props: ['shadow'] },
|
||||||
|
'el-col': { template: '<div><slot/></div>', props: ['xs', 'sm'] },
|
||||||
|
'el-row': { template: '<div><slot/></div>', props: ['gutter'] },
|
||||||
|
'el-icon': { template: '<svg/>' },
|
||||||
|
'el-tabs': { template: '<div class="el-tabs"><slot/></div>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-tab-pane': { template: '<div class="el-tab-pane"><span class="tab-label">{{label}}</span><slot/></div>', props: ['label', 'name'] },
|
||||||
|
'el-radio-group': { template: '<div class="el-radio-group"><slot/></div>', props: ['modelValue'], emits: ['update:modelValue'] },
|
||||||
|
'el-radio-button': { template: '<label class="el-radio-button"><slot/></label>', props: ['value'] },
|
||||||
|
'el-tag': { template: '<span class="el-tag"><slot/></span>', props: ['type', 'size'] },
|
||||||
|
'el-progress': { template: '<div class="el-progress"/>', props: ['percentage', 'stroke-width', 'color'] },
|
||||||
|
'el-table': { template: '<div class="el-table"><slot/></div>', props: ['data', 'loading', 'stripe', 'border'] },
|
||||||
|
'el-table-column': true,
|
||||||
|
'el-dialog': { template: '<div v-if="modelValue" class="el-dialog"><slot/><slot name="footer"/></div>', props: ['modelValue', 'title', 'width'] },
|
||||||
|
'el-descriptions': { template: '<div class="el-descriptions"><slot/></div>', props: ['column', 'border'] },
|
||||||
|
'el-descriptions-item': { template: '<div class="el-descriptions-item"><slot/></div>', props: ['label', 'span'] },
|
||||||
|
'el-avatar': { template: '<div class="el-avatar"/>', props: ['src', 'size'] },
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (wrapper) wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 组件初始化 ====================
|
||||||
|
|
||||||
|
it('应该渲染统计面板容器', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.statistics-dashboard').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染时间区间选择器', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('今日')
|
||||||
|
expect(wrapper.html()).toContain('本周')
|
||||||
|
expect(wrapper.html()).toContain('本月')
|
||||||
|
expect(wrapper.html()).toContain('今年')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染导出报表按钮', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('导出报表')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染两个 Tab 页', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('数据总览')
|
||||||
|
expect(wrapper.html()).toContain('教练业绩')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
it('初始 loading 状态为 false', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始选中的区间应为 today', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.selectedRange).toBe('today')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 activeTab 应为 overview', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.activeTab).toBe('overview')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始 exporting 为 false', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.exporting).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 方法存在性 ====================
|
||||||
|
|
||||||
|
it('应该暴露 onRangeChange 方法', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.onRangeChange).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 onTabChange 方法', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.onTabChange).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 showCoachDetail 方法', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.showCoachDetail).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该暴露 handleExport 方法', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(typeof wrapper.vm.handleExport).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== rateColor 函数 ====================
|
||||||
|
|
||||||
|
it('rateColor 应在 rate >= 80 时返回绿色', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.rateColor(85)).toBe('#67c23a')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rateColor 应在 rate >= 50 时返回橙色', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.rateColor(60)).toBe('#e6a23c')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rateColor 应在 rate < 50 时返回红色', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.rateColor(30)).toBe('#f56c6c')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rateColor inverse=true 应在低取消率时返回绿色', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.rateColor(10, true)).toBe('#67c23a')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== scoreProgressColor 函数 ====================
|
||||||
|
|
||||||
|
it('scoreProgressColor 应正确返回颜色', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.scoreProgressColor(90)).toBe('#67c23a')
|
||||||
|
expect(wrapper.vm.scoreProgressColor(60)).toBe('#e6a23c')
|
||||||
|
expect(wrapper.vm.scoreProgressColor(30)).toBe('#f56c6c')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== getQueryParams ====================
|
||||||
|
|
||||||
|
it('getQueryParams 应返回正确的 periodType', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.vm.selectedRange = 'today'
|
||||||
|
expect(wrapper.vm.getQueryParams().periodType).toBe('DAY')
|
||||||
|
|
||||||
|
wrapper.vm.selectedRange = 'week'
|
||||||
|
expect(wrapper.vm.getQueryParams().periodType).toBe('WEEK')
|
||||||
|
|
||||||
|
wrapper.vm.selectedRange = 'month'
|
||||||
|
expect(wrapper.vm.getQueryParams().periodType).toBe('MONTH')
|
||||||
|
|
||||||
|
wrapper.vm.selectedRange = 'last30'
|
||||||
|
expect(wrapper.vm.getQueryParams().periodType).toBe('LAST_30_DAYS')
|
||||||
|
|
||||||
|
wrapper.vm.selectedRange = 'last90'
|
||||||
|
expect(wrapper.vm.getQueryParams().periodType).toBe('LAST_90_DAYS')
|
||||||
|
|
||||||
|
wrapper.vm.selectedRange = 'year'
|
||||||
|
expect(wrapper.vm.getQueryParams().periodType).toBe('YEAR')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 统计卡片区域 ====================
|
||||||
|
|
||||||
|
it('应该渲染会员总数标签', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('会员总数')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染活跃会员标签', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('活跃会员')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染预约总数标签', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('预约总数')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染教练违规标签', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('教练违规')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 分区标题 ====================
|
||||||
|
|
||||||
|
it('应该渲染会员统计分区', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('会员统计')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染预约统计分区', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('预约统计')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染签到统计分区', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('签到统计')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该渲染教练统计分区', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.html()).toContain('教练统计')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== coach 详情弹窗 ====================
|
||||||
|
|
||||||
|
it('初始 coachDetailVisible 应为 false', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.coachDetailVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('showCoachDetail 应打开详情弹窗', async () => {
|
||||||
|
wrapper = mount(StatisticsDashboard, {
|
||||||
|
global: { stubs: defaultStubs },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
const coach = { coachName: 'Test Coach', completedCourses: 10, compositeScore: 85 }
|
||||||
|
wrapper.vm.showCoachDetail(coach)
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.coachDetailVisible).toBe(true)
|
||||||
|
expect(wrapper.vm.selectedCoach).toEqual(coach)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { UserStatus, RoleStatus, MenuStatus, NoticeStatus, StatusHelper } from '@/constants/status'
|
||||||
|
|
||||||
|
describe('Status Enums', () => {
|
||||||
|
describe('UserStatus', () => {
|
||||||
|
it('should have ACTIVE = 1', () => {
|
||||||
|
expect(UserStatus.ACTIVE).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have INACTIVE = 0', () => {
|
||||||
|
expect(UserStatus.INACTIVE).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have LOCKED = 2', () => {
|
||||||
|
expect(UserStatus.LOCKED).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('RoleStatus', () => {
|
||||||
|
it('should have ACTIVE = 1', () => {
|
||||||
|
expect(RoleStatus.ACTIVE).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have INACTIVE = 0', () => {
|
||||||
|
expect(RoleStatus.INACTIVE).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('MenuStatus', () => {
|
||||||
|
it('should have ACTIVE = 1', () => {
|
||||||
|
expect(MenuStatus.ACTIVE).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have INACTIVE = 0', () => {
|
||||||
|
expect(MenuStatus.INACTIVE).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('NoticeStatus', () => {
|
||||||
|
it('should have ACTIVE = "1"', () => {
|
||||||
|
expect(NoticeStatus.ACTIVE).toBe('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have INACTIVE = "0"', () => {
|
||||||
|
expect(NoticeStatus.INACTIVE).toBe('0')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('StatusHelper', () => {
|
||||||
|
describe('isActive', () => {
|
||||||
|
it('should return true for numeric 1', () => {
|
||||||
|
expect(StatusHelper.isActive(1)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for string "1"', () => {
|
||||||
|
expect(StatusHelper.isActive('1')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for "ACTIVE"', () => {
|
||||||
|
expect(StatusHelper.isActive('ACTIVE')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for numeric 0', () => {
|
||||||
|
expect(StatusHelper.isActive(0)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for string "0"', () => {
|
||||||
|
expect(StatusHelper.isActive('0')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for numeric 2 (LOCKED)', () => {
|
||||||
|
expect(StatusHelper.isActive(2)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isInactive', () => {
|
||||||
|
it('should return true for numeric 0', () => {
|
||||||
|
expect(StatusHelper.isInactive(0)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for string "0"', () => {
|
||||||
|
expect(StatusHelper.isInactive('0')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for "INACTIVE"', () => {
|
||||||
|
expect(StatusHelper.isInactive('INACTIVE')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for numeric 1', () => {
|
||||||
|
expect(StatusHelper.isInactive(1)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for string "1"', () => {
|
||||||
|
expect(StatusHelper.isInactive('1')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getStatusText', () => {
|
||||||
|
it('should return "正常" for active status', () => {
|
||||||
|
expect(StatusHelper.getStatusText(1)).toBe('正常')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return "禁用" for inactive status', () => {
|
||||||
|
expect(StatusHelper.getStatusText(0)).toBe('禁用')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return "未知" for unknown status', () => {
|
||||||
|
expect(StatusHelper.getStatusText(999)).toBe('未知')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getStatusType', () => {
|
||||||
|
it('should return "success" for active status', () => {
|
||||||
|
expect(StatusHelper.getStatusType(1)).toBe('success')
|
||||||
|
expect(StatusHelper.getStatusType('1')).toBe('success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return "danger" for inactive status', () => {
|
||||||
|
expect(StatusHelper.getStatusType(0)).toBe('danger')
|
||||||
|
expect(StatusHelper.getStatusType('0')).toBe('danger')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return "warning" for unknown status', () => {
|
||||||
|
expect(StatusHelper.getStatusType(999)).toBe('warning')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { formatDateTime, formatDate, formatTime } from '@/utils/dateFormat'
|
||||||
|
|
||||||
|
describe('dateFormat', () => {
|
||||||
|
describe('formatDateTime', () => {
|
||||||
|
it('should return dash for null', () => {
|
||||||
|
expect(formatDateTime(null)).toBe('-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return dash for undefined', () => {
|
||||||
|
expect(formatDateTime(undefined)).toBe('-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format ISO string correctly', () => {
|
||||||
|
const result = formatDateTime('2025-01-15T10:30:00.000Z')
|
||||||
|
expect(result).toContain('2025')
|
||||||
|
expect(result).toContain('01')
|
||||||
|
expect(result).toContain('15')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format Date object', () => {
|
||||||
|
const date = new Date(2025, 0, 15, 10, 30, 0)
|
||||||
|
const result = formatDateTime(date)
|
||||||
|
expect(result).toContain('2025')
|
||||||
|
expect(result).toContain('01')
|
||||||
|
expect(result).toContain('15')
|
||||||
|
expect(result).toContain('10:30:00')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return dash for invalid date string', () => {
|
||||||
|
const result = formatDateTime('not-a-date')
|
||||||
|
expect(result).toBe('-')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatDate', () => {
|
||||||
|
it('should return dash for null', () => {
|
||||||
|
expect(formatDate(null)).toBe('-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return dash for undefined', () => {
|
||||||
|
expect(formatDate(undefined)).toBe('-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format date only, no time', () => {
|
||||||
|
const result = formatDate('2025-06-20')
|
||||||
|
expect(result).toBe('2025-06-20')
|
||||||
|
expect(result).not.toContain(':')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format Date object to date only', () => {
|
||||||
|
const date = new Date(2025, 5, 20)
|
||||||
|
const result = formatDate(date)
|
||||||
|
expect(result).toBe('2025-06-20')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return dash for invalid input', () => {
|
||||||
|
const result = formatDate('abc')
|
||||||
|
expect(result).toBe('-')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatTime', () => {
|
||||||
|
it('should return dash for null', () => {
|
||||||
|
expect(formatTime(null)).toBe('-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return dash for undefined', () => {
|
||||||
|
expect(formatTime(undefined)).toBe('-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format time only', () => {
|
||||||
|
const result = formatTime('2025-01-15T14:30:45')
|
||||||
|
expect(result).toBe('14:30:45')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format Date object to time only', () => {
|
||||||
|
const date = new Date(2025, 0, 15, 9, 5, 30)
|
||||||
|
const result = formatTime(date)
|
||||||
|
expect(result).toBe('09:05:30')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return dash for invalid input', () => {
|
||||||
|
const result = formatTime('')
|
||||||
|
expect(result).toBe('-')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
|
||||||
|
// Mock localStorage
|
||||||
|
const localStorageMock = (() => {
|
||||||
|
let store: Record<string, string> = {}
|
||||||
|
return {
|
||||||
|
getItem: (key: string) => store[key] || null,
|
||||||
|
setItem: (key: string, value: string) => { store[key] = value },
|
||||||
|
removeItem: (key: string) => { delete store[key] },
|
||||||
|
clear: () => { store = {} },
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true })
|
||||||
|
|
||||||
|
// Must import after mock
|
||||||
|
import { checkApiPermission, getRequiredPermission } from '@/utils/permission'
|
||||||
|
|
||||||
|
describe('permission utils', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageMock.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getRequiredPermission', () => {
|
||||||
|
it('should return correct permission for exact match', () => {
|
||||||
|
expect(getRequiredPermission('GET', '/users')).toBe('system:user:list')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return correct permission for POST', () => {
|
||||||
|
expect(getRequiredPermission('POST', '/users')).toBe('system:user:add')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return correct permission for DELETE', () => {
|
||||||
|
expect(getRequiredPermission('DELETE', '/roles')).toBe('system:role:remove')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should strip query string from URL', () => {
|
||||||
|
expect(getRequiredPermission('GET', '/users?page=1&size=10')).toBe('system:user:list')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null for unmapped URL', () => {
|
||||||
|
expect(getRequiredPermission('GET', '/unknown')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should uppercase method', () => {
|
||||||
|
expect(getRequiredPermission('get', '/users')).toBe('system:user:list')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('checkApiPermission', () => {
|
||||||
|
it('should return true when no permission data in localStorage', () => {
|
||||||
|
expect(checkApiPermission('GET', '/users')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true when user has required permission', () => {
|
||||||
|
localStorageMock.setItem('permission', JSON.stringify({ permissions: ['system:user:list'] }))
|
||||||
|
expect(checkApiPermission('GET', '/users')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when user lacks required permission', () => {
|
||||||
|
localStorageMock.setItem('permission', JSON.stringify({ permissions: ['system:role:list'] }))
|
||||||
|
expect(checkApiPermission('GET', '/users')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for /menus GET (always allowed)', () => {
|
||||||
|
localStorageMock.setItem('permission', JSON.stringify({ permissions: [] }))
|
||||||
|
expect(checkApiPermission('GET', '/menus')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for unmapped endpoints', () => {
|
||||||
|
localStorageMock.setItem('permission', JSON.stringify({ permissions: [] }))
|
||||||
|
expect(checkApiPermission('GET', '/unmapped-endpoint')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should strip query params before matching', () => {
|
||||||
|
localStorageMock.setItem('permission', JSON.stringify({ permissions: ['system:user:list'] }))
|
||||||
|
expect(checkApiPermission('GET', '/users?page=1&sort=name')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support array-style required permissions (any match)', () => {
|
||||||
|
localStorageMock.setItem('permission', JSON.stringify({ permissions: ['system:config:list'] }))
|
||||||
|
expect(checkApiPermission('GET', '/config')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true on JSON parse error', () => {
|
||||||
|
localStorageMock.setItem('permission', 'invalid-json')
|
||||||
|
expect(checkApiPermission('GET', '/users')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be case-insensitive for method', () => {
|
||||||
|
localStorageMock.setItem('permission', JSON.stringify({ permissions: ['system:user:list'] }))
|
||||||
|
expect(checkApiPermission('get', '/users')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { generateSignature, generateSignatureHeaders } from '@/utils/signature'
|
||||||
|
|
||||||
|
describe('signature', () => {
|
||||||
|
describe('generateSignature', () => {
|
||||||
|
it('should generate a non-empty signature', () => {
|
||||||
|
const sig = generateSignature('GET', '/api/users', '', '', 1700000000000, 'test-nonce')
|
||||||
|
expect(sig).toBeTruthy()
|
||||||
|
expect(typeof sig).toBe('string')
|
||||||
|
expect(sig.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should generate consistent signatures for same input', () => {
|
||||||
|
const sig1 = generateSignature('POST', '/api/data', 'page=1', '{"key":"value"}', 1700000000000, 'abc123')
|
||||||
|
const sig2 = generateSignature('POST', '/api/data', 'page=1', '{"key":"value"}', 1700000000000, 'abc123')
|
||||||
|
expect(sig1).toBe(sig2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should generate different signatures for different methods', () => {
|
||||||
|
const sig1 = generateSignature('GET', '/api/test', '', '', 1700000000000, 'n1')
|
||||||
|
const sig2 = generateSignature('POST', '/api/test', '', '', 1700000000000, 'n1')
|
||||||
|
expect(sig1).not.toBe(sig2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should generate different signatures for different paths', () => {
|
||||||
|
const sig1 = generateSignature('GET', '/api/users', '', '', 1700000000000, 'n1')
|
||||||
|
const sig2 = generateSignature('GET', '/api/roles', '', '', 1700000000000, 'n1')
|
||||||
|
expect(sig1).not.toBe(sig2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should generate different signatures for different nonces', () => {
|
||||||
|
const sig1 = generateSignature('GET', '/api/test', '', '', 1700000000000, 'nonce-1')
|
||||||
|
const sig2 = generateSignature('GET', '/api/test', '', '', 1700000000000, 'nonce-2')
|
||||||
|
expect(sig1).not.toBe(sig2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty query and body', () => {
|
||||||
|
const sig = generateSignature('GET', '/api/simple', '', '', 1700000000000, 'simple')
|
||||||
|
expect(sig).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be base64-encoded', () => {
|
||||||
|
const sig = generateSignature('GET', '/api/test', '', '', 1700000000000, 'test')
|
||||||
|
// Base64 only contains specific characters
|
||||||
|
expect(/^[A-Za-z0-9+/=]+$/.test(sig)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('generateSignatureHeaders', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(1700000000000)
|
||||||
|
vi.spyOn(Math, 'random').mockReturnValue(0.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return headers with correct shape', () => {
|
||||||
|
const headers = generateSignatureHeaders('GET', '/api/users')
|
||||||
|
expect(headers).toHaveProperty('X-Signature')
|
||||||
|
expect(headers).toHaveProperty('X-Timestamp')
|
||||||
|
expect(headers).toHaveProperty('X-Nonce')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set X-Timestamp to current time', () => {
|
||||||
|
const headers = generateSignatureHeaders('GET', '/api/test')
|
||||||
|
expect(headers['X-Timestamp']).toBe('1700000000000')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set X-Nonce to a non-empty string', () => {
|
||||||
|
const headers = generateSignatureHeaders('GET', '/api/test')
|
||||||
|
expect(headers['X-Nonce']).toBeTruthy()
|
||||||
|
expect(headers['X-Nonce'].length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle full URL with https', () => {
|
||||||
|
const headers = generateSignatureHeaders('POST', 'https://example.com/api/data?page=1')
|
||||||
|
expect(headers['X-Signature']).toBeTruthy()
|
||||||
|
expect(headers['X-Timestamp']).toBe('1700000000000')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle relative URL without query', () => {
|
||||||
|
const headers = generateSignatureHeaders('GET', '/api/simple')
|
||||||
|
expect(headers['X-Signature']).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should uppercase the method', () => {
|
||||||
|
const headers = generateSignatureHeaders('get', '/api/test')
|
||||||
|
expect(headers['X-Signature']).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should generate same headers for identical calls', () => {
|
||||||
|
vi.spyOn(Math, 'random').mockReturnValue(0.12345)
|
||||||
|
|
||||||
|
const h1 = generateSignatureHeaders('GET', '/api/test')
|
||||||
|
const h2 = generateSignatureHeaders('GET', '/api/test')
|
||||||
|
expect(h1['X-Signature']).toBe(h2['X-Signature'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { defineConfig } from 'vitest/config'
|
import { defineConfig } from 'vitest/config'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
publicDir: 'public',
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
@@ -36,10 +38,16 @@ export default defineConfig({
|
|||||||
branches: 80,
|
branches: 80,
|
||||||
statements: 80,
|
statements: 80,
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
deps: {
|
||||||
|
inline: ['element-plus'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'/icon': path.resolve(__dirname, 'public/icon'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"adminToken": "eyJhbGciOiJIUzM4NCJ9.eyJyb2xlcyI6WyJhZG1pbiJdLCJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJzdWIiOiJhZG1pbiIsImlhdCI6MTc4NDcxODUyNiwiZXhwIjoxNzg0ODA0OTI2fQ.WQY3IoWxEIT7Qn0xQft2uZjiItcasuAxF297MCaCHYbQXebCClJUoz9Y4_UicE2i",
|
"adminToken": "eyJhbGciOiJIUzM4NCJ9.eyJyb2xlcyI6WyJhZG1pbiJdLCJ0ZW5hbnRJZCI6ImRlZmF1bHQiLCJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJzdWIiOiJhZG1pbiIsImlhdCI6MTc4NDgwODI0OCwiZXhwIjoxNzg0ODk0NjQ4fQ.HMmMZFVaboJrr0qk3LwhwINrqANBLFmc3YWBIMUakNqRrPkGQMFnveOI96pOX67R",
|
||||||
"courseName": "UI全流程_mrvzbyr2",
|
"courseName": "UI全流程_mrxgrkuo",
|
||||||
"timestamp": 1784718529454,
|
"timestamp": 1784808277584,
|
||||||
"baseUrl": "http://192.168.110.64:8084"
|
"baseUrl": "http://192.168.110.64:8084"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user